move move frontend to progress-test
This commit is contained in:
24
app/javascript/pages/dashboard/Dashboard.tsx
Normal file
24
app/javascript/pages/dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, {FC,} from 'react'
|
||||
|
||||
import {DashboardProvider} from './DashboardContext'
|
||||
import {
|
||||
QuestionsBySubject,
|
||||
QuestionByBloomTaxonomy,
|
||||
QuestionsByDifficulty,
|
||||
QuestionByCheckType,
|
||||
} from './charts'
|
||||
import {Filters} from './Filters'
|
||||
|
||||
export const Dashboard: FC = () => (
|
||||
<DashboardProvider>
|
||||
<main className="max-h-screen sm:px-8 gap-2 pt-2 sm:pt-4">
|
||||
<Filters/>
|
||||
<div className="pt-3 grid gap-2 grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 3xl:grid-cols-4">
|
||||
<QuestionsBySubject/>
|
||||
<QuestionByBloomTaxonomy/>
|
||||
<QuestionsByDifficulty/>
|
||||
<QuestionByCheckType/>
|
||||
</div>
|
||||
</main>
|
||||
</DashboardProvider>
|
||||
)
|
||||
47
app/javascript/pages/dashboard/DashboardContext.tsx
Normal file
47
app/javascript/pages/dashboard/DashboardContext.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useMemo,
|
||||
FC,
|
||||
useContext,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from 'react'
|
||||
import {QuestionWhereInput} from '../../__generated__/graphql-schema'
|
||||
import {UserContext, useCurrentUser} from "../../contexts";
|
||||
|
||||
type ProviderValue = {
|
||||
where: QuestionWhereInput
|
||||
setWhere: Dispatch<SetStateAction<QuestionWhereInput>>
|
||||
}
|
||||
|
||||
const DashboardContext = createContext<ProviderValue | null>(null)
|
||||
|
||||
export const useDashboardContext = () => {
|
||||
const context = useContext(DashboardContext)
|
||||
|
||||
if (context === null) {
|
||||
throw new Error('You probably forgot to put <DashboardProvider>.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const whereDefaultState = (userContext: UserContext) => (
|
||||
userContext.isOnlyTeacher ? {userId: userContext.user?.id} : {}
|
||||
)
|
||||
|
||||
export const DashboardProvider: FC = ({children}) => {
|
||||
const userContext = useCurrentUser()
|
||||
const [where, setWhere] = useState<QuestionWhereInput>(whereDefaultState(userContext))
|
||||
const providerValue = useMemo(() => ({where, setWhere}), [
|
||||
where,
|
||||
setWhere,
|
||||
])
|
||||
|
||||
return (
|
||||
<DashboardContext.Provider value={providerValue}>
|
||||
{children}
|
||||
</DashboardContext.Provider>
|
||||
)
|
||||
}
|
||||
155
app/javascript/pages/dashboard/Filters.tsx
Normal file
155
app/javascript/pages/dashboard/Filters.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, {FC, Fragment} from 'react'
|
||||
import {Disclosure, Transition} from "@headlessui/react"
|
||||
import {ChevronDownIcon, XIcon} from "@heroicons/react/outline"
|
||||
import {useForm} from "react-hook-form"
|
||||
|
||||
import {QuestionWhereInput} from "../../__generated__/graphql-schema"
|
||||
import {useDashboardContext, whereDefaultState} from "./DashboardContext"
|
||||
import {useCurrentUser} from "../../contexts"
|
||||
import {Button, Input} from "../../components"
|
||||
|
||||
type FilterBarForm = {
|
||||
fromOtherUsers?: boolean
|
||||
createDate: {
|
||||
startAt: string
|
||||
endAt: string
|
||||
}
|
||||
}
|
||||
|
||||
const startDateISO8601Date = '2021-01-01'
|
||||
const currentISO8601Date = new Date().toISOString().split('T')[0]
|
||||
|
||||
const formDefaultValues: FilterBarForm = {
|
||||
fromOtherUsers: false,
|
||||
createDate: {
|
||||
startAt: startDateISO8601Date,
|
||||
endAt: currentISO8601Date
|
||||
}
|
||||
}
|
||||
|
||||
const mapFilter = (values: FilterBarForm, userId?: string): QuestionWhereInput => ({
|
||||
userId: values.fromOtherUsers ? undefined : userId,
|
||||
createDate: {
|
||||
startAt: values.createDate.startAt.length ? values.createDate.startAt : startDateISO8601Date,
|
||||
endAt: values.createDate.endAt.length ? values.createDate.endAt : currentISO8601Date,
|
||||
}
|
||||
})
|
||||
|
||||
const FiltersForm: FC = () => {
|
||||
const {register, handleSubmit, reset, getValues, formState} = useForm({
|
||||
defaultValues: formDefaultValues,
|
||||
})
|
||||
const {setWhere} = useDashboardContext()
|
||||
const userContext = useCurrentUser()
|
||||
const {user, isOnlyTeacher} = userContext
|
||||
|
||||
const onSubmit = (values: FilterBarForm) => {
|
||||
reset(getValues(), {
|
||||
isDirty: false
|
||||
})
|
||||
setWhere(mapFilter(values, user?.id))
|
||||
}
|
||||
|
||||
const handleClean = () => {
|
||||
reset(formDefaultValues)
|
||||
setWhere(whereDefaultState(userContext))
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className={"flex justify-between"}
|
||||
>
|
||||
<span>
|
||||
<label className={"pl-2 pt-2"}>Data de Criação</label>
|
||||
<div className={"grid grid-cols-2 gap-2 border p-2 m-2 rounded-md border-gray-300"}>
|
||||
<Input
|
||||
type="date"
|
||||
placeholder="createDate.startAt"
|
||||
ref={register({
|
||||
maxLength: 10,
|
||||
minLength: 10,
|
||||
})}
|
||||
name={"createDate.startAt"}
|
||||
label={"A Partir De"}
|
||||
/>
|
||||
<Input
|
||||
type="date"
|
||||
placeholder="createDate.endAt"
|
||||
ref={register({
|
||||
maxLength: 10,
|
||||
minLength: 10,
|
||||
})}
|
||||
name={"createDate.endAt"}
|
||||
label={"Até"}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
{!isOnlyTeacher && (
|
||||
<span className={"flex items-center"}>
|
||||
<label
|
||||
htmlFor={"fromOtherUsers"}
|
||||
children={"Apenas questões próprias?"}
|
||||
className={"mr-3"}
|
||||
/>
|
||||
<input
|
||||
id={"fromOtherUsers"}
|
||||
type="checkbox"
|
||||
placeholder="fromOtherUsers"
|
||||
ref={register}
|
||||
name={"fromOtherUsers"}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<div className={"grid grid-cols-2 gap-2 place-items-center"}>
|
||||
<div>
|
||||
<Button type={'tertiary'} onClick={handleClean}>
|
||||
<span className={"flex"}>
|
||||
<XIcon className={"w-5 h-5 text-gray-800"}/>
|
||||
Limpar filtro
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={!formState.isDirty} type={'primary'} htmlType={"submit"} className={"w-full"}>
|
||||
Filtar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const Filters: FC = () => (
|
||||
<Disclosure>
|
||||
{({open}) => (
|
||||
<div className="m-auto bg-white rounded-md shadow-sm hover:shadow transition-shadow duration-300">
|
||||
<Disclosure.Button as={Fragment}>
|
||||
<button className="flex p-2 w-full justify-between">
|
||||
<div className="grid place-items-center pl-4">
|
||||
Filtros
|
||||
</div>
|
||||
<div className={"pr-4"}>
|
||||
<ChevronDownIcon
|
||||
className={`${open ? 'transform rotate-180' : ''} w-5 h-5 text-gray-800`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel className={"p-4"}>
|
||||
<FiltersForm/>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, {FC} from 'react'
|
||||
import {gql, useQuery} from '@apollo/client'
|
||||
|
||||
import {Query} from '../../../__generated__/graphql-schema'
|
||||
import {Pie} from '../components/charts'
|
||||
import {useDashboardContext} from "../DashboardContext";
|
||||
|
||||
type ResponsivePieData = {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
}[]
|
||||
|
||||
type QuestionsByBloomTaxonomyCountQuery = {
|
||||
remember: Query['questions']
|
||||
understand: Query['questions']
|
||||
apply: Query['questions']
|
||||
analyze: Query['questions']
|
||||
evaluate: Query['questions']
|
||||
create: Query['questions']
|
||||
}
|
||||
|
||||
const QuestionsByBloomTaxonomyCount = gql`
|
||||
query QuestionsByBloomTaxonomyCount (
|
||||
$rememberWhere: QuestionWhereInput!,
|
||||
$understandWhere: QuestionWhereInput!,
|
||||
$applyWhere: QuestionWhereInput!,
|
||||
$analyzeWhere: QuestionWhereInput!,
|
||||
$evaluateWhere: QuestionWhereInput!,
|
||||
$createWhere: QuestionWhereInput!,
|
||||
) {
|
||||
remember: questions(where: $rememberWhere) {
|
||||
totalCount
|
||||
}
|
||||
understand: questions(where: $understandWhere) {
|
||||
totalCount
|
||||
}
|
||||
apply: questions(where: $applyWhere) {
|
||||
totalCount
|
||||
}
|
||||
analyze: questions(where: $analyzeWhere) {
|
||||
totalCount
|
||||
}
|
||||
evaluate: questions(where: $evaluateWhere) {
|
||||
totalCount
|
||||
}
|
||||
create: questions(where: $createWhere) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const QuestionByBloomTaxonomy: FC = () => {
|
||||
const {where} = useDashboardContext()
|
||||
const {loading, data} = useQuery<QuestionsByBloomTaxonomyCountQuery>(
|
||||
QuestionsByBloomTaxonomyCount, {
|
||||
variables: {
|
||||
rememberWhere: {bloomTaxonomy: ['remember'], ...where},
|
||||
understandWhere: {bloomTaxonomy: ['understand'], ...where},
|
||||
applyWhere: {bloomTaxonomy: ['apply'], ...where},
|
||||
analyzeWhere: {bloomTaxonomy: ['analyze'], ...where},
|
||||
evaluateWhere: {bloomTaxonomy: ['evaluate'], ...where},
|
||||
createWhere: {bloomTaxonomy: ['create'], ...where},
|
||||
}
|
||||
})
|
||||
|
||||
if (loading || !data) return null
|
||||
|
||||
const mappedData: ResponsivePieData = [
|
||||
{
|
||||
id: "Recordar",
|
||||
label: "Recordar",
|
||||
value: data.remember.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Compreender",
|
||||
label: "Compreender",
|
||||
value: data.understand.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Aplicar",
|
||||
label: "Aplicar",
|
||||
value: data.apply.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Analisar",
|
||||
label: "Analisar",
|
||||
value: data.analyze.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Avaliar",
|
||||
label: "Avaliar",
|
||||
value: data.evaluate.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Criar",
|
||||
label: "Criar",
|
||||
value: data.create.totalCount ?? 0
|
||||
},
|
||||
]
|
||||
const filteredData = mappedData.filter(item => item.value)
|
||||
|
||||
return (
|
||||
<Pie title="Questões por Habilidade Cognitiva" data={filteredData}/>
|
||||
)
|
||||
}
|
||||
150
app/javascript/pages/dashboard/charts/QuestionsByCheckType.tsx
Normal file
150
app/javascript/pages/dashboard/charts/QuestionsByCheckType.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, {FC} from 'react'
|
||||
import {gql, useQuery} from '@apollo/client'
|
||||
|
||||
import {Query} from '../../../__generated__/graphql-schema'
|
||||
import {Pie} from '../components/charts'
|
||||
import {useDashboardContext} from "../DashboardContext";
|
||||
|
||||
type ResponsivePieData = {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
}[]
|
||||
|
||||
type QuestionsByCheckTypeCountQuery = {
|
||||
uniqueAnswer: Query['questions']
|
||||
incompleteAffirmation: Query['questions']
|
||||
multipleAnswer: Query['questions']
|
||||
negativeFocus: Query['questions']
|
||||
assertionAndReason: Query['questions']
|
||||
gap: Query['questions']
|
||||
interpretation: Query['questions']
|
||||
association: Query['questions']
|
||||
orderingOrRanking: Query['questions']
|
||||
constantAlternatives: Query['questions']
|
||||
}
|
||||
|
||||
const QuestionsByCheckTypeCount = gql`
|
||||
query QuestionsByCheckTypeCount(
|
||||
$uniqueAnswer: QuestionWhereInput!,
|
||||
$incompleteAffirmation: QuestionWhereInput!,
|
||||
$multipleAnswer: QuestionWhereInput!,
|
||||
$negativeFocus: QuestionWhereInput!,
|
||||
$assertionAndReason: QuestionWhereInput!,
|
||||
$gap: QuestionWhereInput!,
|
||||
$interpretation: QuestionWhereInput!,
|
||||
$association: QuestionWhereInput!,
|
||||
$orderingOrRanking: QuestionWhereInput!,
|
||||
$constantAlternatives: QuestionWhereInput!,
|
||||
) {
|
||||
uniqueAnswer: questions(where: $uniqueAnswer) {
|
||||
totalCount
|
||||
}
|
||||
incompleteAffirmation: questions(where: $incompleteAffirmation) {
|
||||
totalCount
|
||||
}
|
||||
multipleAnswer: questions(where: $multipleAnswer) {
|
||||
totalCount
|
||||
}
|
||||
negativeFocus: questions(where: $negativeFocus) {
|
||||
totalCount
|
||||
}
|
||||
assertionAndReason: questions(where: $assertionAndReason) {
|
||||
totalCount
|
||||
}
|
||||
gap: questions(where: $gap) {
|
||||
totalCount
|
||||
}
|
||||
interpretation: questions(where: $interpretation) {
|
||||
totalCount
|
||||
}
|
||||
association: questions(where: $association) {
|
||||
totalCount
|
||||
}
|
||||
orderingOrRanking: questions(where: $orderingOrRanking) {
|
||||
totalCount
|
||||
}
|
||||
constantAlternatives: questions(where: $constantAlternatives) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const QuestionByCheckType: FC = () => {
|
||||
const {where} = useDashboardContext()
|
||||
const {loading, data} = useQuery<QuestionsByCheckTypeCountQuery>(
|
||||
QuestionsByCheckTypeCount, {
|
||||
variables: {
|
||||
uniqueAnswer: {checkType: ['unique_answer'], ...where},
|
||||
incompleteAffirmation: {checkType: ['incomplete_affirmation'], ...where},
|
||||
multipleAnswer: {checkType: ['multiple_answer'], ...where},
|
||||
negativeFocus: {checkType: ['negative_focus'], ...where},
|
||||
assertionAndReason: {checkType: ['assertion_and_reason'], ...where},
|
||||
gap: {checkType: ['gap'], ...where},
|
||||
interpretation: {checkType: ['interpretation'], ...where},
|
||||
association: {checkType: ['association'], ...where},
|
||||
orderingOrRanking: {checkType: ['ordering_or_ranking'], ...where},
|
||||
constantAlternatives: {checkType: ['constant_alternatives'], ...where},
|
||||
}
|
||||
})
|
||||
|
||||
if (loading || !data) return null
|
||||
|
||||
const mappedData: ResponsivePieData = [
|
||||
{
|
||||
id: "Asserção e Razão",
|
||||
label: "Asserção e Razão",
|
||||
value: data.assertionAndReason.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Associação",
|
||||
label: "Associação",
|
||||
value: data.association.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Alternativas Constantes",
|
||||
label: "Alternativas Constantes",
|
||||
value: data.constantAlternatives.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Lacuna",
|
||||
label: "Lacuna",
|
||||
value: data.gap.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Afirmação Incompleta",
|
||||
label: "Afirmação Incompleta",
|
||||
value: data.incompleteAffirmation.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Interpretação",
|
||||
label: "Interpretação",
|
||||
value: data.interpretation.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Resposta Múltipla",
|
||||
label: "Resposta Múltipla",
|
||||
value: data.multipleAnswer.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Foco Negativo",
|
||||
label: "Foco Negativo",
|
||||
value: data.negativeFocus.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Ordenação ou Seriação",
|
||||
label: "Ordenação ou Seriação",
|
||||
value: data.orderingOrRanking.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Resposta Única",
|
||||
label: "Resposta Única",
|
||||
value: data.uniqueAnswer.totalCount ?? 0
|
||||
},
|
||||
]
|
||||
const filteredData = mappedData.filter(item => item.value)
|
||||
|
||||
return (
|
||||
<Pie title="Questões por Tipo" data={filteredData}/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, {FC} from 'react'
|
||||
import {gql, useQuery} from '@apollo/client'
|
||||
|
||||
import {Query} from '../../../__generated__/graphql-schema'
|
||||
import {Pie} from '../components/charts'
|
||||
import {useDashboardContext} from "../DashboardContext";
|
||||
|
||||
type ResponsivePieData = {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
}[]
|
||||
|
||||
type QuestionsByDifficultyCountQuery = {
|
||||
easy: Query['questions']
|
||||
medium: Query['questions']
|
||||
hard: Query['questions']
|
||||
}
|
||||
|
||||
const QuestionsByDifficultyCount = gql`
|
||||
query QuestionsByDifficultyCount(
|
||||
$easyWhere: QuestionWhereInput!,
|
||||
$mediumWhere: QuestionWhereInput!,
|
||||
$hardWhere: QuestionWhereInput!,
|
||||
) {
|
||||
easy: questions(where: $easyWhere) {
|
||||
totalCount
|
||||
}
|
||||
medium: questions(where: $mediumWhere) {
|
||||
totalCount
|
||||
}
|
||||
hard: questions(where: $hardWhere) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const QuestionsByDifficulty: FC = () => {
|
||||
const {where} = useDashboardContext()
|
||||
const {loading, data} = useQuery<QuestionsByDifficultyCountQuery>(
|
||||
QuestionsByDifficultyCount, {
|
||||
variables: {
|
||||
easyWhere: {difficulty: ['easy'], ...where},
|
||||
mediumWhere: {difficulty: ['medium'], ...where},
|
||||
hardWhere: {difficulty: ['hard'], ...where},
|
||||
},
|
||||
})
|
||||
|
||||
if (loading || !data) return null
|
||||
|
||||
const mappedData: ResponsivePieData = [
|
||||
{
|
||||
id: "Fácil",
|
||||
label: "Fácil",
|
||||
value: data.easy.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Difícil",
|
||||
label: "Difícil",
|
||||
value: data.hard.totalCount ?? 0
|
||||
},
|
||||
{
|
||||
id: "Média",
|
||||
label: "Média",
|
||||
value: data.medium.totalCount ?? 0
|
||||
},
|
||||
]
|
||||
const filteredData = mappedData.filter(item => item.value)
|
||||
|
||||
return (
|
||||
<Pie title="Questões por Dificuldade" data={filteredData}/>
|
||||
)
|
||||
}
|
||||
50
app/javascript/pages/dashboard/charts/QuestionsBySubject.tsx
Normal file
50
app/javascript/pages/dashboard/charts/QuestionsBySubject.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, {FC} from 'react'
|
||||
import {gql, useQuery} from '@apollo/client'
|
||||
|
||||
import {Query} from '../../../__generated__/graphql-schema'
|
||||
import {Pie} from '../components/charts'
|
||||
import {useDashboardContext} from "../DashboardContext";
|
||||
|
||||
type ResponsivePieData = {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
}[]
|
||||
|
||||
const QuestionsBySubjectCount = gql`
|
||||
query QuestionsBySubjectCount($where: QuestionWhereInput!) {
|
||||
subjects {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
questions(where: $where) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const QuestionsBySubject: FC = () => {
|
||||
const {where} = useDashboardContext()
|
||||
const {loading, data} = useQuery<Query>(QuestionsBySubjectCount, {
|
||||
variables: {
|
||||
where,
|
||||
},
|
||||
})
|
||||
|
||||
if (loading) return null
|
||||
|
||||
const subjects = data?.subjects.nodes ?? []
|
||||
const subjectWithQuestions = subjects.filter(subject => !!subject?.questions.totalCount)
|
||||
const mappedData: ResponsivePieData = subjectWithQuestions.map(subject => ({
|
||||
id: subject.name,
|
||||
label: subject.name,
|
||||
value: subject.questions.totalCount,
|
||||
}))
|
||||
const filteredData = mappedData.filter(item => item.value)
|
||||
|
||||
return (
|
||||
<Pie title="Questões por Assunto" data={filteredData}/>
|
||||
)
|
||||
}
|
||||
4
app/javascript/pages/dashboard/charts/index.ts
Normal file
4
app/javascript/pages/dashboard/charts/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./QuestionsBySubject";
|
||||
export * from "./QuestionsByBloomTaxonomy";
|
||||
export * from "./QuestionsByDifficulty";
|
||||
export * from "./QuestionsByCheckType";
|
||||
38
app/javascript/pages/dashboard/components/charts/Pie.tsx
Normal file
38
app/javascript/pages/dashboard/components/charts/Pie.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {ResponsivePie} from '@nivo/pie'
|
||||
import React, {FC} from 'react'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
data: {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export const Pie: FC<Props> = ({title, data}) => {
|
||||
return (
|
||||
<div
|
||||
className="m-auto bg-white rounded-md p-4 shadow-sm hover:shadow transition-shadow duration-300"
|
||||
style={{ height: '36rem', width: '36rem' }}
|
||||
>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">{title}</h3>
|
||||
<ResponsivePie
|
||||
data={data}
|
||||
margin={{top: 40, right: 80, bottom: 80, left: 80}}
|
||||
innerRadius={0.5}
|
||||
padAngle={0.7}
|
||||
cornerRadius={3}
|
||||
activeOuterRadiusOffset={8}
|
||||
borderWidth={1}
|
||||
borderColor={{from: 'color', modifiers: [['darker', 0.2]]}}
|
||||
arcLinkLabelsSkipAngle={10}
|
||||
arcLinkLabelsTextColor="#333333"
|
||||
arcLinkLabelsThickness={2}
|
||||
arcLinkLabelsColor={{from: 'color'}}
|
||||
arcLabelsSkipAngle={10}
|
||||
arcLabelsTextColor={{from: 'color', modifiers: [['darker', 2]]}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./Pie";
|
||||
1
app/javascript/pages/dashboard/components/index.ts
Normal file
1
app/javascript/pages/dashboard/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./charts";
|
||||
1
app/javascript/pages/dashboard/index.ts
Normal file
1
app/javascript/pages/dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Dashboard";
|
||||
127
app/javascript/pages/question/Edit/Edit.tsx
Normal file
127
app/javascript/pages/question/Edit/Edit.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, {FC, useState} from 'react'
|
||||
import {useHistory, useParams} from 'react-router';
|
||||
import {gql, useMutation, useQuery} from '@apollo/client';
|
||||
|
||||
import {Mutation, Query, Question} from '../../../__generated__/graphql-schema';
|
||||
import {AlertV2Props, Navigator} from '../../../components';
|
||||
import {Form, FormFragments} from '../Form'
|
||||
import {NodeId} from '../../../utils/graphql';
|
||||
import {QuestionRoutePaths} from "../../../routes";
|
||||
|
||||
const GET_QUESTION = gql`
|
||||
${FormFragments}
|
||||
query Question ($id: ID!) {
|
||||
node (id: $id) {
|
||||
__typename
|
||||
...on Question {
|
||||
id
|
||||
...FormFields
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UPDATE_QUESTION_MUTATOIN = gql`
|
||||
mutation($input: UpdateQuestionInput!) {
|
||||
updateQuestion(input: $input) {
|
||||
question {
|
||||
id
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Edit: FC = () => {
|
||||
const history = useHistory()
|
||||
const [alert, setAlert] = useState<AlertV2Props>()
|
||||
const params = useParams<{ id: string }>()
|
||||
const [updateQuestion] = useMutation<Mutation>(UPDATE_QUESTION_MUTATOIN)
|
||||
const {loading, data} = useQuery<Query>(
|
||||
GET_QUESTION, {
|
||||
variables: {
|
||||
id: params.id,
|
||||
fetchPolicy: "no-cache"
|
||||
}
|
||||
}
|
||||
)
|
||||
const question = data?.node as Question | null
|
||||
|
||||
if (loading || !question) return null
|
||||
|
||||
const recordId = NodeId.decode(question.id).id
|
||||
|
||||
const onSubmit = (inputs: any) => {
|
||||
updateQuestion({
|
||||
variables: {
|
||||
input: {
|
||||
question: {
|
||||
...inputs,
|
||||
id: recordId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
history.push(QuestionRoutePaths.index)
|
||||
}).catch((error: string) => {
|
||||
setAlert({
|
||||
severity: "error",
|
||||
text: `Erro ao atualizar questão. ${error}. Por favor, tente novamente.`,
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => setAlert({severity: "error", text: ""}),
|
||||
3000
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
const onDraftSubmit = (inputs: any) => {
|
||||
updateQuestion({
|
||||
variables: {
|
||||
input: {
|
||||
question: {
|
||||
...inputs,
|
||||
id: recordId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}).then(() => {
|
||||
setAlert({
|
||||
severity: "success",
|
||||
text: "Rascunho atualizado com sucesso",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setAlert(undefined)
|
||||
}, 3000);
|
||||
|
||||
}).catch((error: string) => {
|
||||
setAlert({
|
||||
severity: "error",
|
||||
text: `Erro ao atualizar rascunho. ${error}`,
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => setAlert(undefined),
|
||||
8000
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navigator home/>
|
||||
<div className="bg-gray-100 w-full my-2">
|
||||
<main>
|
||||
<Form
|
||||
question={question}
|
||||
onSubmit={onSubmit}
|
||||
onDraftSubmit={onDraftSubmit}
|
||||
alert={alert}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
app/javascript/pages/question/Edit/index.ts
Normal file
1
app/javascript/pages/question/Edit/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Edit";
|
||||
222
app/javascript/pages/question/Form/Form.tsx
Normal file
222
app/javascript/pages/question/Form/Form.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, {FC, useState} from 'react'
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {ExclamationCircleIcon} from '@heroicons/react/outline';
|
||||
import {useHistory} from "react-router-dom";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {gql} from '@apollo/client';
|
||||
|
||||
import {Question, QuestionCreateInput, QuestionStatus} from '../../../__generated__/graphql-schema';
|
||||
import {formatInput} from '../formatInputs';
|
||||
import {validateQuestionInputs} from '../../../utils/questions/questionValidations';
|
||||
import {RootState} from '../../../services/store';
|
||||
import {FormProvider} from './FormContext'
|
||||
import {SteppedForm, Step} from './SteppedForm'
|
||||
import {
|
||||
EnunciationFormStep,
|
||||
EnunciationFragment,
|
||||
AnswerFormStep,
|
||||
AnswerFragment,
|
||||
DistractorsFormStep,
|
||||
DistractorsFragment,
|
||||
FeaturesFormStep,
|
||||
FeaturesFragment,
|
||||
} from './steps'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
AlertV2Props,
|
||||
AlertV2,
|
||||
List,
|
||||
ListItem,
|
||||
} from '../../../components';
|
||||
import {QuestionRoutePaths} from "../../../routes";
|
||||
import {turnOff, turnOn} from "../../../services/store/unsavedChanges";
|
||||
|
||||
export const FormFragments = gql`
|
||||
${EnunciationFragment}
|
||||
${AnswerFragment}
|
||||
${DistractorsFragment}
|
||||
${FeaturesFragment}
|
||||
fragment FormFields on Question {
|
||||
...EnunciationFields
|
||||
...AnswerFields
|
||||
...DistractorsFields
|
||||
...FeaturesFields
|
||||
status
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
question?: Question
|
||||
onSubmit?: (inputs: any) => void
|
||||
onDraftSubmit?: (inputs: any) => void
|
||||
alert?: AlertV2Props
|
||||
}
|
||||
|
||||
export const Form: FC<Props> = ({question, onSubmit, onDraftSubmit, alert}) => {
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([])
|
||||
const [confirmSaveDialogIsOpen, setConfirmFinishDialogIsOpen] = useState(false)
|
||||
const [leaveDialogIsOpen, setLeaveDialogIsOpen] = useState(false)
|
||||
const {register, control, setValue, getValues, reset, formState} = useForm()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||
const history = useHistory()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const minStep = 0
|
||||
const maxStep = 3
|
||||
const onFirstStep = currentStep === minStep
|
||||
const onLastStep = currentStep === maxStep
|
||||
|
||||
if (formState.isDirty) {
|
||||
dispatch(turnOn())
|
||||
}
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (onLastStep) return
|
||||
|
||||
setCurrentStep(currentStep + 1)
|
||||
}
|
||||
|
||||
const handlePreviousStep = () => {
|
||||
if (onFirstStep) return
|
||||
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
|
||||
const getFormattedInputValues = () => formatInput(getValues())
|
||||
|
||||
const handleCancel = () => {
|
||||
if (unsavedChanges && !leaveDialogIsOpen) {
|
||||
setLeaveDialogIsOpen(true)
|
||||
} else {
|
||||
history.push(QuestionRoutePaths.index)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDraftSave = () => {
|
||||
if (onDraftSubmit) {
|
||||
onDraftSubmit({...getFormattedInputValues(), status: QuestionStatus.Draft} as QuestionCreateInput)
|
||||
reset(getValues(), {
|
||||
isDirty: false
|
||||
})
|
||||
dispatch(turnOff())
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const inputs = {...getFormattedInputValues(), status: QuestionStatus.WaitingReview} as QuestionCreateInput
|
||||
const errors = validateQuestionInputs(inputs)
|
||||
|
||||
setConfirmFinishDialogIsOpen(false)
|
||||
|
||||
if (onSubmit && !errors.length) {
|
||||
dispatch(turnOff())
|
||||
onSubmit(inputs)
|
||||
} else {
|
||||
setValidationErrors(errors)
|
||||
}
|
||||
|
||||
reset(getValues(), {
|
||||
isDirty: false
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider props={{question, hooks: {register, control, setValue}}}>
|
||||
{alert && (
|
||||
<AlertV2 severity={alert.severity} text={alert.text}></AlertV2>
|
||||
)}
|
||||
<Dialog
|
||||
isOpen={leaveDialogIsOpen}
|
||||
setIsOpen={setLeaveDialogIsOpen}
|
||||
onConfirmation={handleCancel}
|
||||
title="Modificações não Salvas"
|
||||
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={confirmSaveDialogIsOpen}
|
||||
setIsOpen={setConfirmFinishDialogIsOpen}
|
||||
onConfirmation={handleSave}
|
||||
title="Modificações não Salvas"
|
||||
text="Ao finalizar a questão, o revisor receberá uma notificação para revisá-la. Deseja continuar?"
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={!!validationErrors.length}
|
||||
setIsOpen={() => setValidationErrors([])}
|
||||
onConfirmation={() => setValidationErrors([])}
|
||||
title="Falha de Validação"
|
||||
type="notice"
|
||||
text={
|
||||
<>
|
||||
<List>
|
||||
{validationErrors?.map((errorMessage) => (
|
||||
<ListItem
|
||||
key={errorMessage}
|
||||
icon={<ExclamationCircleIcon className="w-5 text-gray-800"/>}
|
||||
text={errorMessage}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<form className="m-auto max-w-screen-md">
|
||||
<SteppedForm
|
||||
currentStep={currentStep}
|
||||
className="mb-3"
|
||||
>
|
||||
<Step step={0}>
|
||||
<EnunciationFormStep/>
|
||||
</Step>
|
||||
<Step step={1}>
|
||||
<AnswerFormStep/>
|
||||
</Step>
|
||||
<Step step={2}>
|
||||
<DistractorsFormStep/>
|
||||
</Step>
|
||||
<Step step={3}>
|
||||
<FeaturesFormStep/>
|
||||
</Step>
|
||||
</SteppedForm>
|
||||
|
||||
<div
|
||||
className="mx-3 sm:mx-0 flex justify-items-center flex-col-reverse sm:flex-row justify-end space-x-0 sm:space-x-2">
|
||||
<Button
|
||||
className={"mb-3 sm:mb-0"}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
className={`mb-3 sm:mb-0 ${onFirstStep ? "hidden" : ""}`}
|
||||
onClick={handlePreviousStep}
|
||||
>
|
||||
Retornar
|
||||
</Button>
|
||||
{(question?.status === QuestionStatus.Draft || question?.status === undefined) &&
|
||||
<Button className={"mb-3 sm:mb-0"} onClick={handleDraftSave}>
|
||||
Salvar Rascunho
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
type="primary"
|
||||
className={`mb-3 sm:mb-0 ${onLastStep ? "hidden" : ""}`}
|
||||
onClick={handleNextStep}
|
||||
>
|
||||
Prosseguir
|
||||
</Button>
|
||||
{onLastStep &&
|
||||
<Button
|
||||
type="primary"
|
||||
className="mb-3 sm:mb-0"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Finalizar
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
39
app/javascript/pages/question/Form/FormContext.tsx
Normal file
39
app/javascript/pages/question/Form/FormContext.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { FC, useContext } from 'react'
|
||||
import { Control, FieldValues } from 'react-hook-form';
|
||||
import { Question } from '../../../__generated__/graphql-schema';
|
||||
|
||||
type FormContextHooks = {
|
||||
register: any
|
||||
setValue: Function
|
||||
control: Control<FieldValues>
|
||||
}
|
||||
|
||||
type FormContextProps = {
|
||||
hooks: FormContextHooks
|
||||
question?: Question
|
||||
}
|
||||
|
||||
const FormContext = React.createContext<FormContextProps | null>(null);
|
||||
|
||||
export const useFormProvider = () => {
|
||||
const context = useContext(FormContext)
|
||||
|
||||
if (context === null) {
|
||||
throw new Error('You probably forgot to put <FormProvider>.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: any
|
||||
props: FormContextProps
|
||||
}
|
||||
|
||||
export const FormProvider: FC<Props> = ({ children, props }) => {
|
||||
return (
|
||||
<FormContext.Provider value={props}>
|
||||
{children}
|
||||
</FormContext.Provider>
|
||||
)
|
||||
}
|
||||
34
app/javascript/pages/question/Form/SteppedForm.tsx
Normal file
34
app/javascript/pages/question/Form/SteppedForm.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { FC } from "react";
|
||||
|
||||
type StepProps = {
|
||||
children: any
|
||||
step: number
|
||||
}
|
||||
|
||||
export const Step: FC<StepProps> = ({ children }) => (children);
|
||||
|
||||
type Props = {
|
||||
children: any;
|
||||
currentStep: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SteppedForm: FC<Props> = ({
|
||||
children,
|
||||
currentStep,
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
{children?.map((x: any) => {
|
||||
const visible = x.props.step === currentStep;
|
||||
|
||||
return (
|
||||
<div key={x.props.step} className={visible ? "" : "hidden"}>
|
||||
{x}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
55
app/javascript/pages/question/Form/components/TextEditor.tsx
Normal file
55
app/javascript/pages/question/Form/components/TextEditor.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { FC } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import CKEditor from "@ckeditor/ckeditor5-react";
|
||||
import * as ClassicEditor from "ckeditor5-mathtype/build/ckeditor";
|
||||
|
||||
import { useFormProvider } from '../FormContext'
|
||||
|
||||
const toolbarOptions = [
|
||||
"bold",
|
||||
"italic",
|
||||
"blockQuote",
|
||||
"numberedList",
|
||||
"bulletedList",
|
||||
"imageUpload",
|
||||
"insertTable",
|
||||
"tableColumn",
|
||||
"tableRow",
|
||||
"mergeTableCells",
|
||||
"|",
|
||||
"MathType",
|
||||
"ChemType",
|
||||
"|",
|
||||
"undo",
|
||||
"redo",
|
||||
];
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
defaultValue: string
|
||||
}
|
||||
|
||||
export const TextEditor: FC<Props> = ({ name, defaultValue }) => {
|
||||
const { hooks: { control } } = useFormProvider()
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
defaultValue={defaultValue}
|
||||
render={({ onChange, value }) => (
|
||||
<CKEditor
|
||||
editor={ClassicEditor}
|
||||
data={value}
|
||||
config={{
|
||||
toolbar: toolbarOptions,
|
||||
ckfinder: {
|
||||
uploadUrl: `${process.env.REACT_APP_BACKEND_URL}/uploads`,
|
||||
},
|
||||
}}
|
||||
onChange={(_: any, editor: any) => onChange(editor.getData())}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
1
app/javascript/pages/question/Form/index.ts
Normal file
1
app/javascript/pages/question/Form/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Form";
|
||||
61
app/javascript/pages/question/Form/steps/AnswerFormStep.tsx
Normal file
61
app/javascript/pages/question/Form/steps/AnswerFormStep.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import React, { FC } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
import { Card } from "../../../../components/Card/Card";
|
||||
import { TextEditor } from "../components/TextEditor";
|
||||
import { useFormProvider } from '../FormContext'
|
||||
|
||||
export const AnswerFragment = gql`
|
||||
fragment AnswerFields on Question {
|
||||
alternatives {
|
||||
correct
|
||||
text
|
||||
}
|
||||
explanation
|
||||
references
|
||||
}
|
||||
`
|
||||
|
||||
export const AnswerFormStep: FC = () => {
|
||||
const { question, hooks: { control } } = useFormProvider()
|
||||
|
||||
const alternativesMaped = question?.alternatives || [
|
||||
{ text: "", correct: true },
|
||||
];
|
||||
|
||||
const correctAlternative = alternativesMaped.find(
|
||||
(alternative) => alternative.correct === true,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="Resposta Correta" className="mb-3">
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full">
|
||||
<TextEditor
|
||||
name={`alternatives[0].text`}
|
||||
defaultValue={correctAlternative?.text ?? ''}
|
||||
/>
|
||||
<Controller
|
||||
name={`alternatives[0].correct`}
|
||||
control={control}
|
||||
defaultValue={true}
|
||||
render={() => (<></>)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full border border-gray-300 rounded p-4 mt-4 shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">Explicação</h2>
|
||||
<TextEditor name="explanation" defaultValue={question?.explanation ?? ''} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">Referências</h2>
|
||||
<TextEditor defaultValue={question?.references ?? ''} name="references" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { FC } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
import { Card } from "../../../../components";
|
||||
import { TextEditor } from "../components/TextEditor";
|
||||
import { useFormProvider } from '../FormContext'
|
||||
|
||||
export const DistractorsFragment = gql`
|
||||
fragment DistractorsFields on Question {
|
||||
alternatives {
|
||||
correct
|
||||
text
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const DistractorsFormStep: FC = () => {
|
||||
const { question, hooks: { control } } = useFormProvider()
|
||||
|
||||
const incorrectAnswers = question?.alternatives?.filter(
|
||||
(alternative) => alternative.correct === false,
|
||||
) || [
|
||||
{ text: "", correct: false },
|
||||
{ text: "", correct: false },
|
||||
{ text: "", correct: false },
|
||||
{ text: "", correct: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="Distratores">
|
||||
<div className="flex flex-col">
|
||||
<div className="">
|
||||
{incorrectAnswers.map(({ text }, index) => (
|
||||
<div className="w-full mb-3" key={index}>
|
||||
<TextEditor
|
||||
name={`alternatives[${index + 1}].text`}
|
||||
defaultValue={text ?? ""}
|
||||
/>
|
||||
<Controller
|
||||
name={`alternatives[${index + 1}].correct`}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={() => (<></>)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { Card } from "../../../../components";
|
||||
import { TextEditor } from '../components/TextEditor'
|
||||
import { useFormProvider } from '../FormContext'
|
||||
|
||||
export const EnunciationFragment = gql`
|
||||
fragment EnunciationFields on Question {
|
||||
instruction
|
||||
support
|
||||
body
|
||||
}
|
||||
`
|
||||
|
||||
export const EnunciationFormStep: FC = () => {
|
||||
const { question } = useFormProvider()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full mb-3" title="Instrução (opcional)">
|
||||
<TextEditor name="instruction" defaultValue={question?.instruction ?? ""} />
|
||||
</Card>
|
||||
<Card className="h-full mb-3" title="Suporte (opcional)">
|
||||
<TextEditor name="support" defaultValue={question?.support ?? ""} />
|
||||
</Card>
|
||||
<Card className="h-full mb-3" title="Enunciado">
|
||||
<TextEditor name="body" defaultValue={question?.body ?? ""} />
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
import { Card } from "../../../../../components";
|
||||
import { SubjectSelect, SubjectFragment } from "./SubjectSelect";
|
||||
import { ReviewerSelect, ReviewerFragment } from "./ReviewSelect";
|
||||
import { useFormProvider } from '../../FormContext'
|
||||
|
||||
import { BLOOM_TAXONOMY, CHECK_TYPE, DIFFICULTY } from "../../../../../utils/types";
|
||||
import { Question } from "../../../../../__generated__/graphql-schema";
|
||||
|
||||
export const FeaturesFragment = gql`
|
||||
${ReviewerFragment}
|
||||
${SubjectFragment}
|
||||
fragment FeaturesFields on Question {
|
||||
... ReviewerFields
|
||||
... SubjectFields
|
||||
authorship
|
||||
authorshipYear
|
||||
difficulty
|
||||
checkType
|
||||
intention
|
||||
bloomTaxonomy
|
||||
}
|
||||
`
|
||||
|
||||
export const FeaturesFormStep: FC = () => {
|
||||
const { question, hooks: { setValue, register } } = useFormProvider();
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const {
|
||||
authorship,
|
||||
authorshipYear,
|
||||
difficulty,
|
||||
bloomTaxonomy,
|
||||
checkType,
|
||||
} = question || {} as Question
|
||||
|
||||
const [ownQuestion, setOwnQuestion] = useState<boolean>(authorship === "UNIFESO" || authorship === undefined || authorship === null);
|
||||
|
||||
const handleOwnCheck = (value: string) => {
|
||||
if (value === 'UNIFESO') {
|
||||
setOwnQuestion(true)
|
||||
setValue("authorship", "UNIFESO");
|
||||
setValue("authorshipYear", currentYear.toString());
|
||||
} else {
|
||||
setOwnQuestion(false)
|
||||
setValue("authorship", "");
|
||||
setValue("authorshipYear", "");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="Características">
|
||||
<div className="grid grid-cols-2 col-gap-2">
|
||||
<div className="flex">
|
||||
<label htmlFor="own" className="mr-3 my-auto">
|
||||
Autoria
|
||||
</label>
|
||||
<div className="my-auto">
|
||||
<input
|
||||
className="my-auto"
|
||||
type="radio"
|
||||
id="authorship-own"
|
||||
checked={!!ownQuestion}
|
||||
ref={register}
|
||||
onChange={() => handleOwnCheck("UNIFESO")}
|
||||
name="__nonused"
|
||||
/>
|
||||
<label htmlFor="authorship-own" className="ml-1">Própria</label>
|
||||
</div>
|
||||
<div className="my-auto ml-3">
|
||||
<input
|
||||
className="my-auto"
|
||||
type="radio"
|
||||
id="authorship-third"
|
||||
checked={!ownQuestion}
|
||||
ref={register}
|
||||
onChange={() => handleOwnCheck("")}
|
||||
name="__nonused"
|
||||
/>
|
||||
<label htmlFor="authorship-third" className="ml-1">Outro</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex">
|
||||
<h2 className="pr-2 my-auto">Fonte</h2>
|
||||
<div className="w-full">
|
||||
<div style={{ maxWidth: "194px" }}>
|
||||
<input
|
||||
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||
ref={register}
|
||||
name="authorship"
|
||||
defaultValue={authorship || (ownQuestion ? "UNIFESO" : "")}
|
||||
readOnly={!!ownQuestion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<h2 className="pr-2 pl-3 my-auto">Ano</h2>
|
||||
<div style={{ maxWidth: "62px" }}>
|
||||
<input
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
ref={register}
|
||||
type="number"
|
||||
min="1999"
|
||||
max={currentYear}
|
||||
step="1"
|
||||
name="authorshipYear"
|
||||
defaultValue={authorshipYear ?? new Date().getFullYear().toString()}
|
||||
readOnly={!!ownQuestion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 col-gap-2 mt-3">
|
||||
<div className="w-full grid grid-cols-1 row-gap-4">
|
||||
<div className="flex flex-col">
|
||||
<h2>Grau de Dificuldade</h2>
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="difficulty"
|
||||
defaultValue={difficulty ?? ""}
|
||||
>
|
||||
<option />
|
||||
{DIFFICULTY.map((item, index) => (
|
||||
<option key={index} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<h2>Tipo</h2>
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="checkType"
|
||||
defaultValue={checkType ?? ""}
|
||||
>
|
||||
<option />
|
||||
{CHECK_TYPE.map((item, index) => (
|
||||
<option key={index} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<h2>Habilidade Cognitiva</h2>
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="bloomTaxonomy"
|
||||
defaultValue={bloomTaxonomy ?? ""}
|
||||
>
|
||||
<option />
|
||||
{BLOOM_TAXONOMY.map((item, index) => (
|
||||
<option key={index} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<SubjectSelect />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mt-4">
|
||||
<h2>Intenção</h2>
|
||||
<textarea
|
||||
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||
ref={register}
|
||||
name="intention"
|
||||
defaultValue={question?.intention ?? ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col mt-4">
|
||||
<h2>Revisor</h2>
|
||||
<ReviewerSelect />
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { FC } from "react";
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
|
||||
import { useFormProvider } from '../../FormContext'
|
||||
import { Query, QuestionStatus, User } from "../../../../../__generated__/graphql-schema";
|
||||
|
||||
export const ReviewerFragment = gql`
|
||||
fragment ReviewerFields on Question {
|
||||
reviewer {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const REVIEWERS_QUERY = gql`
|
||||
query ReviwersQuery {
|
||||
reviewers {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
reviewer?: User
|
||||
}
|
||||
|
||||
export const ReviewerSelect: FC<Props> = () => {
|
||||
const { question, hooks: { register } } = useFormProvider()
|
||||
const { loading, data } = useQuery<Query>(REVIEWERS_QUERY);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
const reviewers = data?.reviewers.nodes
|
||||
|
||||
return (
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="reviewerUserId"
|
||||
defaultValue={question?.reviewer?.id}
|
||||
>
|
||||
{(question?.status === undefined || question?.status === QuestionStatus.Draft) && <option />}
|
||||
{reviewers?.map((review, index) => (
|
||||
<option key={index} value={review?.id}>
|
||||
{review?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { useQuery, gql } from "@apollo/client";
|
||||
|
||||
import { Query } from "../../../../../__generated__/graphql-schema";
|
||||
import { useFormProvider } from '../../FormContext'
|
||||
|
||||
type Props = {
|
||||
subjectId?: string
|
||||
}
|
||||
|
||||
export const SubjectFragment = gql`
|
||||
fragment SubjectFields on Question {
|
||||
subject {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SUBJECTS_QUERY = gql`
|
||||
query SubjectQuery {
|
||||
subjects {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
axis {
|
||||
name
|
||||
}
|
||||
category {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SubjectSelect: FC<Props> = () => {
|
||||
const { question, hooks: { register } } = useFormProvider()
|
||||
const [selectedId, setSelectedId] = useState(question?.subject?.id);
|
||||
|
||||
const { loading, data } = useQuery<Query>(SUBJECTS_QUERY);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
const subjects = data?.subjects.nodes
|
||||
|
||||
const selectedSubject = data?.subjects.nodes?.find((subject) => subject?.id === selectedId);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div>
|
||||
<h2>Assunto</h2>
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="subjectId"
|
||||
defaultValue={question?.subject?.id ?? ""}
|
||||
onChange={(e) => setSelectedId(e.target.value)}
|
||||
>
|
||||
<option value="" />
|
||||
{subjects?.map((subject) => (
|
||||
<option
|
||||
key={`${subject?.name}-${subject?.id}`}
|
||||
value={subject?.id}
|
||||
>
|
||||
{subject?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<span className="mt-4">
|
||||
Eixo de Formação
|
||||
<input
|
||||
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||
disabled
|
||||
value={selectedSubject?.axis.name}
|
||||
/>
|
||||
</span>
|
||||
<span className="mt-4">
|
||||
Categoria
|
||||
<input
|
||||
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
|
||||
disabled
|
||||
value={selectedSubject?.category.name}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./FeaturesFormStep";
|
||||
4
app/javascript/pages/question/Form/steps/index.ts
Normal file
4
app/javascript/pages/question/Form/steps/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./EnunciationFormStep";
|
||||
export * from "./AnswerFormStep";
|
||||
export * from "./DistractoresFormStep";
|
||||
export * from "./FeaturesFromStep";
|
||||
35
app/javascript/pages/question/List/List.tsx
Normal file
35
app/javascript/pages/question/List/List.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
|
||||
import { Navigator } from "../../../components";
|
||||
import { QuestionsFilter } from "./QuestionFilter";
|
||||
import { QuestionsPainel } from "./QuestionsPainel";
|
||||
import { FiltersProvider } from './QuestionFilter/QuestionsFilterProvider'
|
||||
|
||||
export const List: FC = () => {
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<FiltersProvider>
|
||||
<Navigator newQuestion={true}>
|
||||
<li className={"hover:text-white ml-auto"}>
|
||||
<button onClick={() => setFilterOpen(true)} className="flex">
|
||||
<FaFilter className="my-auto" />
|
||||
<span className="pl-3">Filtros</span>
|
||||
</button>
|
||||
</li>
|
||||
</Navigator>
|
||||
<QuestionsFilter
|
||||
isOpen={filterOpen}
|
||||
setIsOpen={setFilterOpen}
|
||||
/>
|
||||
<div className="bg-gray-100 w-full">
|
||||
<main className="sm:px-8 rounded-t-xlg">
|
||||
<div className="mx-2 sm:mx-0 sm:mr-4">
|
||||
<QuestionsPainel />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</FiltersProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { Dispatch, FC, SetStateAction } from "react";
|
||||
|
||||
type Props = {
|
||||
setChanged: Dispatch<SetStateAction<boolean>>
|
||||
register: any
|
||||
}
|
||||
|
||||
export const AuthorshipFilter: FC<Props> = ({ setChanged, register }) => {
|
||||
const options = [
|
||||
{
|
||||
label: "Qualquer",
|
||||
value: 'null'
|
||||
},
|
||||
{
|
||||
label: "Própria",
|
||||
value: 'true'
|
||||
},
|
||||
{
|
||||
label: "Terceiro",
|
||||
value: 'false',
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||
<h3 className="font-bold mb-1">Autoria</h3>
|
||||
<div
|
||||
className="grid grid-cols-2 sm:flex sm:flex-col"
|
||||
key={`filter-group-authorship`}
|
||||
>
|
||||
{options.map(({ value, label }, index) => (
|
||||
<span className="mr-1 mb-2 sm:mb-0 sm:mr-0" key={label}>
|
||||
<input
|
||||
ref={register}
|
||||
type="radio"
|
||||
name="authorship"
|
||||
value={value}
|
||||
id={value}
|
||||
defaultChecked={!index}
|
||||
/>
|
||||
<label htmlFor={value} className="ml-2">
|
||||
{label}
|
||||
</label>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { Dispatch, FC, SetStateAction } from 'react'
|
||||
|
||||
import { range } from '../../../../utils/math'
|
||||
import { useFiltersProvider } from './QuestionsFilterProvider'
|
||||
|
||||
type Props = {
|
||||
register: any
|
||||
setChanged: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const CURRENT_YEAR = new Date().getFullYear()
|
||||
|
||||
const YEARS = range(1900, CURRENT_YEAR + 1).reverse()
|
||||
|
||||
export const QuestionsAuthorshipTypeFilter: FC<Props> = ({ register, setChanged }) => {
|
||||
const { where } = useFiltersProvider()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="authorshipYear"
|
||||
defaultValue={where.authorshipYear ?? ""}
|
||||
onClick={() => setChanged(true)}
|
||||
>
|
||||
<option value="" />
|
||||
{YEARS.map((year) => (
|
||||
<option
|
||||
key={`questionYear-${year}`}
|
||||
value={year}
|
||||
>
|
||||
{year}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import React, { Dispatch, FC, SetStateAction, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
QuestionBloomTaxonomy,
|
||||
QuestionCheckType,
|
||||
QuestionDifficulty,
|
||||
} from "../../../../__generated__/graphql-schema";
|
||||
import { CHECK_TYPE, BLOOM_TAXONOMY, DIFFICULTY } from "../../../../utils/types";
|
||||
|
||||
import { Button, Modal } from "../../../../components";
|
||||
import { useFiltersProvider } from "./QuestionsFilterProvider";
|
||||
import { QuestionsSubjectFilter } from './QuestionsSubjectFilter'
|
||||
import { QuestionsAuthorshipTypeFilter } from "./QuestionsAuthorshipTypeFilter";
|
||||
import { AuthorshipFilter } from "./AuthorshipFilter";
|
||||
|
||||
type FilterGroupProps = {
|
||||
title: string;
|
||||
register: any;
|
||||
options: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
selecteds: any[];
|
||||
setChanged: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const FilterGroup: FC<FilterGroupProps> = ({
|
||||
title,
|
||||
options,
|
||||
register,
|
||||
selecteds,
|
||||
setChanged,
|
||||
}) => (
|
||||
<>
|
||||
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||
<h3 className="font-bold mb-1">{title}</h3>
|
||||
<div
|
||||
className="grid grid-cols-2 sm:flex sm:flex-col"
|
||||
key={`filter-group-${title}`}
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<span className="mr-1 mb-2 sm:mb-0 sm:mr-0" key={value}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={value}
|
||||
ref={register}
|
||||
id={value}
|
||||
defaultChecked={selecteds.includes(value)}
|
||||
onClick={() => setChanged(true)}
|
||||
/>
|
||||
<label htmlFor={value} className="ml-2">
|
||||
{label}
|
||||
</label>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const QuestionsFilter: FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
const { handleSubmit, register, reset } = useForm();
|
||||
const { where, setWhere } = useFiltersProvider();
|
||||
const { difficulty, checkType, bloomTaxonomy } = where;
|
||||
const [changed, setChanged] = useState(false);
|
||||
const submitRef = useRef<HTMLInputElement>()
|
||||
|
||||
const onSubmit = (inputs: any) => {
|
||||
const valuesFromCheckType = CHECK_TYPE.filter(
|
||||
({ value }) => inputs[value]
|
||||
).map(({ value }) => value) as QuestionCheckType[];
|
||||
|
||||
const valuesFromBloomTaxonomy = BLOOM_TAXONOMY.filter(
|
||||
({ value }) => inputs[value]
|
||||
).map(({ value }) => value) as QuestionBloomTaxonomy[];
|
||||
|
||||
const valuesFromDifficulty = DIFFICULTY.filter(
|
||||
({ value }) => inputs[value]
|
||||
).map(({ value }) => value) as QuestionDifficulty[];
|
||||
|
||||
const removeKeysWithUndefiend = (obj: any) => {
|
||||
for (var propName in obj) {
|
||||
if (obj[propName] === null || obj[propName] === undefined) {
|
||||
delete obj[propName];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
setWhere({
|
||||
unifesoAuthorship: inputs.authorship === 'null' ? null : inputs.authorship === 'true',
|
||||
...removeKeysWithUndefiend({
|
||||
checkType: valuesFromCheckType.length ? valuesFromCheckType : undefined,
|
||||
bloomTaxonomy: valuesFromBloomTaxonomy.length
|
||||
? valuesFromBloomTaxonomy
|
||||
: undefined,
|
||||
difficulty: valuesFromDifficulty.length
|
||||
? valuesFromDifficulty
|
||||
: undefined,
|
||||
subjectId: inputs.subjectId === "" ? undefined : inputs.subjectId,
|
||||
authorshipYear: inputs.authorshipYear === "" ? undefined : [inputs.authorshipYear],
|
||||
}),
|
||||
});
|
||||
|
||||
setChanged(false);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleClean = () => {
|
||||
setChanged(false);
|
||||
setWhere({});
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Filtros"
|
||||
setIsOpen={setIsOpen}
|
||||
isOpen={isOpen}
|
||||
buttons={
|
||||
<>
|
||||
<Button
|
||||
onClick={() => handleClean()}
|
||||
disabled={!changed}
|
||||
className={`${changed ? 'opacity-1' : 'opacity-0'}`}
|
||||
>
|
||||
Limpar
|
||||
</Button>
|
||||
<Button onClick={() => setIsOpen(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => submitRef.current?.click()}>
|
||||
Aplicar
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grid grid-cols-1 gap-4 sm:gap-8 lg:grid-cols-2">
|
||||
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||
<h3 className="font-bold mb-1">Assunto</h3>
|
||||
<div className="grid grid-cols-2 sm:flex sm:flex-col">
|
||||
<QuestionsSubjectFilter register={register} setChanged={setChanged} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 sm:mt-0 flex flex-col">
|
||||
<h3 className="font-bold mb-1">Ano</h3>
|
||||
<div className="grid grid-cols-2 sm:flex sm:flex-col">
|
||||
<QuestionsAuthorshipTypeFilter register={register} setChanged={setChanged} />
|
||||
</div>
|
||||
</div>
|
||||
<FilterGroup
|
||||
title="Tipo"
|
||||
register={register}
|
||||
options={CHECK_TYPE}
|
||||
selecteds={(checkType ?? []) as QuestionCheckType[]}
|
||||
setChanged={setChanged}
|
||||
/>
|
||||
<FilterGroup
|
||||
title="Habilidade Cognitiva"
|
||||
register={register}
|
||||
options={BLOOM_TAXONOMY}
|
||||
selecteds={(bloomTaxonomy ?? []) as QuestionBloomTaxonomy[]}
|
||||
setChanged={setChanged}
|
||||
/>
|
||||
<FilterGroup
|
||||
title="Grau de Dificuldade"
|
||||
register={register}
|
||||
options={DIFFICULTY}
|
||||
selecteds={(difficulty ?? []) as QuestionDifficulty[]}
|
||||
setChanged={setChanged}
|
||||
/>
|
||||
<AuthorshipFilter register={register} setChanged={setChanged} />
|
||||
<input hidden type="submit" ref={submitRef as any} />
|
||||
</div>
|
||||
</form>
|
||||
</Modal >
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useMemo,
|
||||
FC,
|
||||
useContext,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from 'react'
|
||||
|
||||
import { QuestionWhereInput } from '../../../../__generated__/graphql-schema'
|
||||
|
||||
type ProviderValue = {
|
||||
where: QuestionWhereInput
|
||||
setWhere: Dispatch<SetStateAction<QuestionWhereInput>>
|
||||
}
|
||||
|
||||
const FiltersContext = createContext<ProviderValue | null>(null)
|
||||
|
||||
export const useFiltersProvider = () => {
|
||||
const context = useContext(FiltersContext)
|
||||
|
||||
if (context === null) {
|
||||
throw new Error('You probably forgot to put <FiltersProvider>.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const FiltersProvider: FC = ({ children }) => {
|
||||
const [where, setWhere] = useState<QuestionWhereInput>({})
|
||||
|
||||
const providerValue = useMemo(() => ({ where, setWhere }), [
|
||||
where,
|
||||
setWhere,
|
||||
])
|
||||
|
||||
return (
|
||||
<FiltersContext.Provider value={providerValue}>
|
||||
{children}
|
||||
</FiltersContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { Dispatch, FC, SetStateAction } from 'react'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
|
||||
import { Query } from '../../../../__generated__/graphql-schema'
|
||||
import { useFiltersProvider } from './QuestionsFilterProvider'
|
||||
|
||||
const SUBJECTS_QUERY = gql`
|
||||
query {
|
||||
subjects {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
register: any
|
||||
setChanged: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const QuestionsSubjectFilter: FC<Props> = ({ register, setChanged }) => {
|
||||
const { where } = useFiltersProvider();
|
||||
const { loading, data } = useQuery<Query>(SUBJECTS_QUERY)
|
||||
|
||||
if (loading) return null
|
||||
|
||||
const subjects = data?.subjects.nodes
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
ref={register}
|
||||
className="w-full rounded p-1 border-gray-400 border shadow-sm"
|
||||
name="subjectId"
|
||||
defaultValue={where.subjectId ?? ""}
|
||||
onClick={() => setChanged(true)}
|
||||
>
|
||||
<option value="" />
|
||||
{subjects?.map((subject) => (
|
||||
<option
|
||||
key={`${subject?.name}-${subject?.id}`}
|
||||
value={subject?.id}
|
||||
>
|
||||
{subject?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./QuestionsFilter";
|
||||
export * from "./QuestionsFilterProvider";
|
||||
131
app/javascript/pages/question/List/QuestionsList.tsx
Normal file
131
app/javascript/pages/question/List/QuestionsList.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { FC, useState } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Question, QuestionStatus } from '../../../__generated__/graphql-schema'
|
||||
import { useCurrentUser } from '../../../contexts';
|
||||
import { NodeId } from '../../../utils/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
const EditIcon = styled(MdModeEdit)`
|
||||
margin: auto;
|
||||
font-size: 1.5rem;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
questions: Question[]
|
||||
title: string
|
||||
pagination?: {
|
||||
onNextPageClick: () => void
|
||||
hasNextPage: boolean
|
||||
hasPreviousPage: boolean
|
||||
onPreviousPageClick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export const QuestionsListFragments = gql`
|
||||
fragment QuestionFields on Question {
|
||||
id
|
||||
status
|
||||
user {
|
||||
id
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
`
|
||||
|
||||
export const QuestionsList: FC<Props> = ({ questions, title, pagination }) => {
|
||||
const { user } = useCurrentUser()
|
||||
const [pageCount, setPageCount] = useState(1)
|
||||
|
||||
const formatDate = (stringDate: string) => new Date(stringDate).toLocaleDateString()
|
||||
|
||||
const handleOnNextPageClick = () => {
|
||||
if (pagination?.hasNextPage) {
|
||||
pagination.onNextPageClick()
|
||||
setPageCount(pageCount + 1)
|
||||
}
|
||||
}
|
||||
const handleOnPreviousPageClick = () => {
|
||||
if (pagination?.hasPreviousPage) {
|
||||
pagination.onPreviousPageClick()
|
||||
setPageCount(pageCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-200 p-4 rounded my-2">
|
||||
<div className="flex">
|
||||
<h2 className="text-gray-500 font-medium text-xl">{title}</h2>
|
||||
<div className="ml-auto text-sm sm:text-base text-gray-700">
|
||||
<button
|
||||
className="p-2"
|
||||
onClick={handleOnPreviousPageClick}
|
||||
style={{ visibility: (pagination?.hasPreviousPage ? 'visible' : 'hidden') }}
|
||||
>
|
||||
<FaArrowLeft />
|
||||
</button>
|
||||
Página: {pageCount}
|
||||
<button
|
||||
className="p-2"
|
||||
onClick={handleOnNextPageClick}
|
||||
style={{ visibility: (pagination?.hasNextPage ? 'visible' : 'hidden') }}
|
||||
>
|
||||
<FaArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="border-t border-gray-400 m-px" />
|
||||
<div className="p-2 text-sm">
|
||||
{questions.length
|
||||
? <div className="flex-col w-full sm:grid gap-4 sm:col-gap-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{questions.map((question) => (
|
||||
<div
|
||||
key={`question-${question.id}`}
|
||||
className="mx-1 sm:mx-0 mb-4 sm:mb-0 border-l-8 border-primary-light flex bg-white hover:bg-unifeso-50 rounded shadow hover:shadow-md cursor-pointer group transition-all duration-500"
|
||||
>
|
||||
<Link
|
||||
className="flex flex-col w-full px-3 py-2"
|
||||
to={`/questions/${question.id}/${(question.user.id === user?.id ? '' : 'review')}`}
|
||||
>
|
||||
<h2>
|
||||
{`# ${NodeId.decode(question.id).id}`}
|
||||
</h2>
|
||||
<div className="text-sm text-gray-700 flex flex-col flex-wrap justify-between">
|
||||
<span>
|
||||
Atualizada em:
|
||||
{" "}
|
||||
{formatDate(question.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
{(question.user.id === user?.id && question.status !== QuestionStatus.Registered) &&
|
||||
<div
|
||||
className="flex flex-col relative flex-grow justify-center"
|
||||
>
|
||||
<Link
|
||||
className="group-hover:block absolute bg-gray-300 hover:bg-primary-normal text-gray-500 hover:text-gray-100 hover:shadow-lg rounded-full p-2 cursor-pointer shadow-inner transition-all duration-500"
|
||||
style={{ left: '-1.5rem' }}
|
||||
to={`/questions/${question.id}/edit`}
|
||||
>
|
||||
<EditIcon />
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
: <div className="grid text-gray-800" style={{ placeItems: 'center' }}>
|
||||
<div className="text-center">
|
||||
<span className="text-sm sm:text-base">
|
||||
Nenhuma questão.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
22
app/javascript/pages/question/List/QuestionsPainel.tsx
Normal file
22
app/javascript/pages/question/List/QuestionsPainel.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React, { FC } from 'react'
|
||||
import { QuestionStatus } from '../../../__generated__/graphql-schema'
|
||||
import { useFiltersProvider } from './QuestionFilter/QuestionsFilterProvider'
|
||||
import { QuestionsQuery } from './QuestionsQuery'
|
||||
import { QuestionsRevisedQuery } from './QuestionsRevisedQuery'
|
||||
import { QuestionsWaitingReviewQuery } from './QuestionsWaitingReviewQuery'
|
||||
|
||||
export const QuestionsPainel: FC = () => {
|
||||
const { where } = useFiltersProvider()
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuestionsWaitingReviewQuery title="Aguardando seu Parecer" />
|
||||
<QuestionsQuery title="Aguardando Parecer do Revisor" where={where} status={QuestionStatus.WaitingReview} />
|
||||
<QuestionsQuery title="Pendentes de Alterações" where={where} status={QuestionStatus.WithRequestedChanges} />
|
||||
<QuestionsQuery title="Rascunhos" where={where} status={QuestionStatus.Draft} />
|
||||
<QuestionsQuery title="Aprovadas" where={where} status={QuestionStatus.Approved} />
|
||||
<QuestionsQuery title="Registradas" where={where} status={QuestionStatus.Registered} />
|
||||
<QuestionsRevisedQuery title="Revisadas por Você" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
95
app/javascript/pages/question/List/QuestionsQuery.tsx
Normal file
95
app/javascript/pages/question/List/QuestionsQuery.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { FC, useState } from 'react'
|
||||
|
||||
import { PageInfo, Query, Question, QuestionWhereInput, QuestionStatus } from '../../../__generated__/graphql-schema';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
|
||||
import { useCurrentUser } from '../../../contexts';
|
||||
|
||||
const QUESTIONS_QUERY = gql`
|
||||
${QuestionsListFragments}
|
||||
query QuestionsQuery($first: Int!, $after: String, $before: String, $where: QuestionWhereInput) {
|
||||
questions (
|
||||
first: $first,
|
||||
after: $after,
|
||||
before: $before,
|
||||
where: $where
|
||||
) {
|
||||
nodes {
|
||||
... QuestionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const PAGE_SIZE = 4
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
where?: QuestionWhereInput
|
||||
status?: QuestionStatus
|
||||
}
|
||||
|
||||
export const QuestionsQuery: FC<Props> = ({ title, where, status }) => {
|
||||
const { user } = useCurrentUser()
|
||||
|
||||
const [questions, setQuestions] = useState<Question[]>([])
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
|
||||
|
||||
const updateQuestions = (queryResult: Query) => {
|
||||
const { questions: questionConnection } = queryResult
|
||||
|
||||
setQuestions(questionConnection.nodes as Question[])
|
||||
setPageInfo(questionConnection.pageInfo)
|
||||
}
|
||||
|
||||
const whereInput = {
|
||||
status,
|
||||
userId: user?.id,
|
||||
...where
|
||||
}
|
||||
|
||||
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
|
||||
onCompleted: (response) => {
|
||||
updateQuestions(response)
|
||||
},
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
where: whereInput,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
})
|
||||
|
||||
const onNextPageClick = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
after: pageInfo?.endCursor,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
updateQuestions(data)
|
||||
})
|
||||
}
|
||||
|
||||
const onPreviousPageClick = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
before: pageInfo?.startCursor,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
updateQuestions(data)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<QuestionsList
|
||||
title={title}
|
||||
questions={questions}
|
||||
pagination={{
|
||||
onNextPageClick: onNextPageClick,
|
||||
hasNextPage: pageInfo?.hasNextPage ?? false,
|
||||
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
|
||||
onPreviousPageClick: onPreviousPageClick
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
89
app/javascript/pages/question/List/QuestionsRevisedQuery.tsx
Normal file
89
app/javascript/pages/question/List/QuestionsRevisedQuery.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { FC, useState } from 'react'
|
||||
|
||||
import { PageInfo, Query, Question, ReviewRequest, User } from '../../../__generated__/graphql-schema';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
|
||||
|
||||
const QUESTIONS_QUERY = gql`
|
||||
${QuestionsListFragments}
|
||||
query QuestionsRevisedQuery($first: Int!, $after: String) {
|
||||
currentUser {
|
||||
id
|
||||
inactiveReviewRequests(
|
||||
first: $first,
|
||||
after: $after
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
question {
|
||||
... QuestionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const PAGE_SIZE = 4
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const QuestionsRevisedQuery: FC<Props> = ({ title }) => {
|
||||
const [questions, setQuestions] = useState<Question[]>([])
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
|
||||
|
||||
const updateQuestions = (queryResult: Query) => {
|
||||
const { currentUser } = queryResult
|
||||
const { inactiveReviewRequests } = currentUser as User
|
||||
const reviewRequests = inactiveReviewRequests.nodes as ReviewRequest[]
|
||||
|
||||
setQuestions(reviewRequests.map(item => item.question))
|
||||
setPageInfo(inactiveReviewRequests.pageInfo)
|
||||
}
|
||||
|
||||
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
|
||||
onCompleted: (response) => {
|
||||
updateQuestions(response)
|
||||
},
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
})
|
||||
|
||||
const onNextPageClick = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
after: pageInfo?.endCursor,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
updateQuestions(data)
|
||||
})
|
||||
}
|
||||
|
||||
const onPreviousPageClick = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
before: pageInfo?.startCursor,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
updateQuestions(data)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<QuestionsList
|
||||
title={title}
|
||||
questions={questions}
|
||||
pagination={{
|
||||
onNextPageClick: onNextPageClick,
|
||||
hasNextPage: pageInfo?.hasNextPage ?? false,
|
||||
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
|
||||
onPreviousPageClick: onPreviousPageClick
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { FC, useState } from 'react'
|
||||
|
||||
import { PageInfo, Query, Question, ReviewRequest, User } from '../../../__generated__/graphql-schema';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
|
||||
|
||||
const QUESTIONS_QUERY = gql`
|
||||
${QuestionsListFragments}
|
||||
query QuestionsWaitingReviewQuery($first: Int!, $after: String) {
|
||||
currentUser {
|
||||
id
|
||||
activeReviewRequests(
|
||||
first: $first,
|
||||
after: $after
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
question {
|
||||
... QuestionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const PAGE_SIZE = 4
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const QuestionsWaitingReviewQuery: FC<Props> = ({ title }) => {
|
||||
const [questions, setQuestions] = useState<Question[]>([])
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
|
||||
|
||||
const updateQuestions = (queryResult: Query) => {
|
||||
const { currentUser } = queryResult
|
||||
const { activeReviewRequests } = currentUser as User
|
||||
const reviewRequests = activeReviewRequests.nodes as ReviewRequest[]
|
||||
|
||||
setQuestions(reviewRequests.map(item => item.question))
|
||||
setPageInfo(activeReviewRequests.pageInfo)
|
||||
}
|
||||
|
||||
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
|
||||
onCompleted: (response) => {
|
||||
updateQuestions(response)
|
||||
},
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
})
|
||||
|
||||
const onNextPageClick = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
after: pageInfo?.endCursor,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
updateQuestions(data)
|
||||
})
|
||||
}
|
||||
|
||||
const onPreviousPageClick = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
first: PAGE_SIZE,
|
||||
before: pageInfo?.startCursor,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
updateQuestions(data)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<QuestionsList
|
||||
title={title}
|
||||
questions={questions}
|
||||
pagination={{
|
||||
onNextPageClick: onNextPageClick,
|
||||
hasNextPage: pageInfo?.hasNextPage ?? false,
|
||||
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
|
||||
onPreviousPageClick: onPreviousPageClick
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
1
app/javascript/pages/question/List/index.ts
Normal file
1
app/javascript/pages/question/List/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { List } from "./List";
|
||||
87
app/javascript/pages/question/New/New.tsx
Normal file
87
app/javascript/pages/question/New/New.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, {useState} from "react";
|
||||
import {useHistory} from "react-router";
|
||||
import {gql, useMutation} from "@apollo/client";
|
||||
|
||||
import {AlertV2Props, Navigator} from "../../../components";
|
||||
import {Form} from '../Form'
|
||||
import {Mutation} from "../../../__generated__/graphql-schema";
|
||||
import {QuestionRoutePaths} from "../../../routes";
|
||||
|
||||
const CREATE_QUESTION_MUTATION = gql`
|
||||
mutation($input: CreateQuestionInput!) {
|
||||
createQuestion(input: $input) {
|
||||
question {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const New = () => {
|
||||
const history = useHistory()
|
||||
const [alert, setAlert] = useState<AlertV2Props>();
|
||||
const [createQuestion] = useMutation<Mutation>(CREATE_QUESTION_MUTATION)
|
||||
|
||||
const onSubmit = (inputs: any) => {
|
||||
createQuestion({
|
||||
variables: {
|
||||
input: {
|
||||
question: inputs,
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
history.push(QuestionRoutePaths.index)
|
||||
}).catch((error: string) => {
|
||||
setAlert({
|
||||
severity: "error",
|
||||
text: `Erro ao criar questão. ${error}. Por favor, tente novamente.`,
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => setAlert({severity: "error", text: ""}),
|
||||
8000
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
const onDraftSubmit = (inputs: any) => {
|
||||
createQuestion({
|
||||
variables: {
|
||||
input: {
|
||||
question: inputs,
|
||||
},
|
||||
}
|
||||
}).then(({data}) => {
|
||||
setAlert({
|
||||
severity: "success",
|
||||
text: "Rascunho criado com sucesso",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const id = data?.createQuestion?.question?.id
|
||||
history.push(QuestionRoutePaths.edit.replace(':id', id ?? ''))
|
||||
}, 3000);
|
||||
}).catch((error: string) => {
|
||||
setAlert({
|
||||
severity: "error",
|
||||
text: `Erro ao criar rascunho. ${error}`,
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => setAlert(undefined),
|
||||
8000
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navigator home={true}/>
|
||||
<div className="bg-gray-100 w-full my-2">
|
||||
<main>
|
||||
<Form onSubmit={onSubmit} onDraftSubmit={onDraftSubmit} alert={alert}/>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
};
|
||||
1
app/javascript/pages/question/New/index.ts
Normal file
1
app/javascript/pages/question/New/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./New";
|
||||
53
app/javascript/pages/question/Review/Review.tsx
Normal file
53
app/javascript/pages/question/Review/Review.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { FC } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
|
||||
import { ViewMode, ViewModeFragments, ReviewMessages, ReviewMessagesFragments } from "../shared";
|
||||
import { Navigator } from "../../../components";
|
||||
import { Query, Question } from "../../../__generated__/graphql-schema";
|
||||
|
||||
export const GET_QUESTION = gql`
|
||||
${ViewModeFragments}
|
||||
${ReviewMessagesFragments}
|
||||
query Question($id: ID!) {
|
||||
node(id: $id) {
|
||||
__typename
|
||||
... on Question {
|
||||
id
|
||||
...QuestionReadOnlyFields
|
||||
...ReviewMessages_question
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Review: FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const { loading, data, refetch } = useQuery<Query>(GET_QUESTION, {
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
})
|
||||
|
||||
const question = data?.node as Question | null
|
||||
|
||||
if (loading || !question) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navigator home />
|
||||
<div className="bg-gray-100 h-full w-full my-2">
|
||||
<main className="flex px-5 max-w-screen-xl m-auto">
|
||||
<div className="w-3/5">
|
||||
<ViewMode questionData={question} />
|
||||
</div>
|
||||
<div className="w-2/5 ml-3">
|
||||
<div className="my-3" />
|
||||
<ReviewMessages question={question} refetch={refetch}/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
app/javascript/pages/question/Review/index.ts
Normal file
1
app/javascript/pages/question/Review/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Review } from "./Review";
|
||||
188
app/javascript/pages/question/Show/Show.tsx
Normal file
188
app/javascript/pages/question/Show/Show.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, {FC, useState} from "react";
|
||||
import {useParams, useHistory} from "react-router-dom";
|
||||
import {MdDeleteForever, MdEdit, MdSave} from "react-icons/md";
|
||||
import {useQuery, useMutation, gql} from "@apollo/client";
|
||||
|
||||
import {ViewMode, ViewModeFragments, ReviewMessages, ReviewMessagesFragments} from "../shared";
|
||||
import {Navigator, Dialog} from "../../../components";
|
||||
import {Mutation, Query, Question, QuestionStatus} from "../../../__generated__/graphql-schema";
|
||||
import {AlertV2Props, AlertV2} from "../../../components/AlertV2";
|
||||
import {NodeId} from "../../../utils/graphql";
|
||||
import {QuestionRoutePaths} from "../../../routes";
|
||||
|
||||
export const GET_QUESTION = gql`
|
||||
${ViewModeFragments}
|
||||
${ReviewMessagesFragments}
|
||||
query Question($id: ID!) {
|
||||
node(id: $id) {
|
||||
__typename
|
||||
... on Question {
|
||||
id
|
||||
...QuestionReadOnlyFields
|
||||
...ReviewMessages_question
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const FINISH_QUESTION = gql`
|
||||
mutation FinishQuestion($id: ID!) {
|
||||
finishQuestion (
|
||||
input: {
|
||||
questionId: $id
|
||||
}
|
||||
) {
|
||||
question {
|
||||
id
|
||||
status
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DESTROY_QUESTION = gql`
|
||||
mutation DestroyQuestion($id: ID!) {
|
||||
destroyQuestion(
|
||||
input: {
|
||||
questionId: $id
|
||||
}
|
||||
) {
|
||||
deletedQuestionId
|
||||
errors
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Show: FC = () => {
|
||||
const history = useHistory();
|
||||
const {id} = useParams<{ id: string }>();
|
||||
const [confirmRegister, setConfirmRegister] = useState(false)
|
||||
const [confirmDestroy, setConfirmDestroy] = useState(false)
|
||||
const [alert, setAlert] = useState<AlertV2Props>()
|
||||
const [finishQuestion] = useMutation<Mutation>(FINISH_QUESTION)
|
||||
const [destroyQuestion] = useMutation<Mutation>(DESTROY_QUESTION)
|
||||
const {loading, data, refetch} = useQuery<Query>(GET_QUESTION, {
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
const question = data?.node as Question | null
|
||||
|
||||
if (loading || !question) return null;
|
||||
|
||||
const recordId = NodeId.decode(question.id).id
|
||||
|
||||
const confirmEditQuestion = () => {
|
||||
history.push(`/questions/${id}/edit`);
|
||||
};
|
||||
|
||||
const handleEditQuestion = () => {
|
||||
confirmEditQuestion()
|
||||
};
|
||||
|
||||
const handleRegisterQuestion = async () => {
|
||||
try {
|
||||
await finishQuestion({variables: {id: recordId}})
|
||||
|
||||
setAlert({
|
||||
text: 'Questão registrada com sucesso!',
|
||||
severity: 'success',
|
||||
})
|
||||
} catch(error){
|
||||
setAlert({
|
||||
text: 'Algo inesperado aconteceu ao tentar registrar a questão.',
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
setConfirmRegister(false)
|
||||
};
|
||||
|
||||
const handleDestroyQuestion = async () => {
|
||||
const {data: questionDestroyData } = await destroyQuestion({variables: {id: recordId}})
|
||||
|
||||
if (questionDestroyData?.destroyQuestion?.deletedQuestionId) {
|
||||
history.push(QuestionRoutePaths.index)
|
||||
} else {
|
||||
setAlert({
|
||||
text: 'Algo inesperado aconteceu ao tentar excluir a questão.',
|
||||
severity: 'error',
|
||||
})
|
||||
setConfirmDestroy(false)
|
||||
}
|
||||
};
|
||||
|
||||
const ACTIONS = {
|
||||
edit: {
|
||||
icon: <MdEdit className="my-auto"/>,
|
||||
label: "Editar",
|
||||
action: handleEditQuestion,
|
||||
},
|
||||
register: {
|
||||
icon: <MdSave className="my-auto"/>,
|
||||
label: "Registrar",
|
||||
action: () => setConfirmRegister(true),
|
||||
},
|
||||
destroy: {
|
||||
icon: <MdDeleteForever className="my-auto"/>,
|
||||
label: 'Excluir',
|
||||
action: () => setConfirmDestroy(true),
|
||||
}
|
||||
}
|
||||
|
||||
const options = (() => {
|
||||
switch (question.status) {
|
||||
case QuestionStatus.Registered:
|
||||
return ([]);
|
||||
case QuestionStatus.Approved:
|
||||
return ([ACTIONS.edit, ACTIONS.register, ACTIONS.destroy])
|
||||
default:
|
||||
return ([ACTIONS.edit, ACTIONS.destroy])
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
isOpen={confirmDestroy}
|
||||
setIsOpen={(value) => setConfirmDestroy(value)}
|
||||
title="Confirmação de Exclusão"
|
||||
text="Após a exclusão, a questão não poderá ser recuperada. Deseja continuar?"
|
||||
onConfirmation={handleDestroyQuestion}
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={confirmRegister}
|
||||
setIsOpen={(value) => setConfirmRegister(value)}
|
||||
title="Confirmação de Registro"
|
||||
text="Após o registro, a questão estará disponível para uso e não poderá mais ser editada ou excluída. Deseja continuar?"
|
||||
onConfirmation={handleRegisterQuestion}
|
||||
/>
|
||||
<Navigator home>
|
||||
{options.map((option, index) => (
|
||||
<div key={`navigation-item-${index}`} className={`hover:text-white ${index === 0 ? "ml-auto" : ""}`}>
|
||||
<button onClick={option.action} className="flex pl-4">
|
||||
{option.icon}
|
||||
<span className="pl-2">{option.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</Navigator>
|
||||
<div className="bg-gray-100 w-full my-2">
|
||||
<main className="max-w-screen-xl m-auto">
|
||||
<div className="flex">
|
||||
{alert && <AlertV2 severity={alert.severity} text={alert.text}/>}
|
||||
</div>
|
||||
<div className="flex px-5">
|
||||
<div className="w-3/5">
|
||||
<ViewMode questionData={question}/>
|
||||
</div>
|
||||
<div className="w-2/5 ml-3">
|
||||
<ReviewMessages question={question} refetch={refetch}/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
app/javascript/pages/question/Show/index.ts
Normal file
1
app/javascript/pages/question/Show/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Show } from "./Show";
|
||||
15
app/javascript/pages/question/formatInputs.ts
Normal file
15
app/javascript/pages/question/formatInputs.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Question } from "../../__generated__/graphql-schema";
|
||||
|
||||
export const formatInput = (inputs: any) =>
|
||||
({
|
||||
...inputs,
|
||||
bloomTaxonomy:
|
||||
inputs.bloomTaxonomy === "" ? undefined : inputs.bloomTaxonomy,
|
||||
difficulty: inputs.difficulty === "" ? undefined : inputs.difficulty,
|
||||
checkType: inputs.checkType === "" ? undefined : inputs.checkType,
|
||||
subjectId: inputs.subjectId === "" ? undefined : inputs.subjectId,
|
||||
reviewerUserId:
|
||||
inputs.reviewerUserId === "" ? undefined : inputs.reviewerUserId,
|
||||
alternatives: inputs.alternatives,
|
||||
__nonused: undefined,
|
||||
} as Question);
|
||||
5
app/javascript/pages/question/index.ts
Normal file
5
app/javascript/pages/question/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./New";
|
||||
export * from "./Show";
|
||||
export * from "./Review";
|
||||
export * from "./Edit";
|
||||
export * from "./List";
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ApolloQueryResult, gql, OperationVariables } from "@apollo/client";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
DocumentRemoveIcon
|
||||
} from '@heroicons/react/outline';
|
||||
import React, { FC } from "react";
|
||||
import { Card } from "../../../../components";
|
||||
import { Query, Question, ReviewMessage, ReviewMessageFeedbackType } from "../../../../__generated__/graphql-schema";
|
||||
import { ReviewMessageForm, ReviewMessageFormFragments } from "./ReviewMessagesForm";
|
||||
|
||||
|
||||
const feedbackIcon = {
|
||||
[ReviewMessageFeedbackType.Answer]: null,
|
||||
[ReviewMessageFeedbackType.Approve]: <CheckCircleIcon className="w-5 text-green-800" />,
|
||||
[ReviewMessageFeedbackType.RequestChanges]: <DocumentRemoveIcon className="w-5 text-red-800" />,
|
||||
};
|
||||
|
||||
const ReviewMessageTitle: FC<{
|
||||
feedback: ReviewMessage
|
||||
}> = ({ feedback }) => (
|
||||
<p className="flex">
|
||||
{feedback.user.name}{' '} - {' '}
|
||||
<span className="text-gray-700 pr-2">
|
||||
{new Date(feedback.createdAt).toLocaleString()}
|
||||
</span>
|
||||
{feedbackIcon[feedback.feedbackType]}
|
||||
</p>
|
||||
)
|
||||
|
||||
export const ReviewMessagesFragments = gql`
|
||||
${ReviewMessageFormFragments}
|
||||
fragment ReviewMessages_question on Question {
|
||||
id
|
||||
...ReviewMessageForm_question
|
||||
user {
|
||||
id
|
||||
}
|
||||
reviewMessages {
|
||||
nodes {
|
||||
id
|
||||
feedbackType
|
||||
text
|
||||
user {
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ReviewMessages: FC<{
|
||||
question: Question
|
||||
refetch: (variables?: Partial<OperationVariables> | undefined) => Promise<ApolloQueryResult<Query>>
|
||||
}> = ({ question, refetch }) => {
|
||||
const reviewMessages = question.reviewMessages.nodes
|
||||
const hasFeebacks = !!reviewMessages.length
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className="mb-3" title="Histórico de Pareceres">
|
||||
{hasFeebacks
|
||||
? reviewMessages.map((item) => (
|
||||
<div key={item.id}>
|
||||
<ReviewMessageTitle feedback={item} />
|
||||
<p className="p-2">
|
||||
{item.text}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
: 'Essa questão não tem nenhum parecer ainda.'}
|
||||
</Card>
|
||||
<ReviewMessageForm question={question} refetch={refetch} />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import { ApolloQueryResult, gql, OperationVariables, useMutation } from "@apollo/client";
|
||||
import React, { FC, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Prompt, useHistory } from "react-router";
|
||||
import { Button, Card } from "../../../../components";
|
||||
import { useCurrentUser } from "../../../../contexts";
|
||||
import { NodeId } from "../../../../utils/graphql";
|
||||
import { Mutation, Query, Question, ReviewMessageFeedbackType } from "../../../../__generated__/graphql-schema";
|
||||
|
||||
export const REVIEW_FEEDBACK = [
|
||||
{
|
||||
label: "Aprovada",
|
||||
description: "O revisor sugere que as observações enviadas no parecer sejam consideradas.",
|
||||
value: ReviewMessageFeedbackType.Approve,
|
||||
},
|
||||
{
|
||||
label: "Pendente de Alterações",
|
||||
description: "O autor deve efetuar as alterações solicitadas no parecer e reenviar a questão ao revisor.",
|
||||
value: ReviewMessageFeedbackType.RequestChanges,
|
||||
},
|
||||
];
|
||||
|
||||
export const ReviewMessageFormFragments = gql`
|
||||
fragment ReviewMessageForm_question on Question {
|
||||
id
|
||||
status
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CREATE_REVIEW_MESSAGE_MUTATION = gql`
|
||||
mutation($questionId: ID!, $feedbackType: ReviewMessageFeedbackType!, $text: String!) {
|
||||
createReviewMessage(
|
||||
input: {
|
||||
message: {
|
||||
questionId: $questionId
|
||||
feedbackType: $feedbackType
|
||||
text: $text
|
||||
}
|
||||
}
|
||||
) {
|
||||
reviewMessage {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ReviewMessageForm: FC<{
|
||||
question: Question
|
||||
refetch: (variables?: Partial<OperationVariables> | undefined) => Promise<ApolloQueryResult<Query>>
|
||||
}> = ({ question, refetch }) => {
|
||||
const [isChangesSaved, setIsChangesSaved] = useState(true)
|
||||
const { register, handleSubmit } = useForm()
|
||||
const history = useHistory();
|
||||
const { user } = useCurrentUser()
|
||||
|
||||
const [createReviewMessage] = useMutation<Mutation['createReviewMessage']>(CREATE_REVIEW_MESSAGE_MUTATION)
|
||||
|
||||
const hasFeebacks = !!question.reviewMessages.nodes.length
|
||||
const questionIsFromCurrentUser = user?.id === question.user.id
|
||||
|
||||
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (e.target.value !== '') {
|
||||
setIsChangesSaved(false)
|
||||
} else {
|
||||
setIsChangesSaved(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitClick = () => {
|
||||
setIsChangesSaved(true)
|
||||
}
|
||||
|
||||
const formSubmit = async (inputs: {
|
||||
feedbackType: ReviewMessageFeedbackType
|
||||
text: string
|
||||
}) => {
|
||||
await createReviewMessage({
|
||||
variables: {
|
||||
text: inputs.text,
|
||||
feedbackType: questionIsFromCurrentUser ? ReviewMessageFeedbackType.Answer : inputs.feedbackType,
|
||||
questionId: NodeId.decode(question.id).id,
|
||||
},
|
||||
});
|
||||
|
||||
await refetch()
|
||||
|
||||
history.push('/questions')
|
||||
};
|
||||
|
||||
if (!hasFeebacks && questionIsFromCurrentUser) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Prompt
|
||||
when={!isChangesSaved}
|
||||
message='O parecer ainda não foi enviado, deseja continuar?'
|
||||
/>
|
||||
<Card title="Parecer" className="max-w-screen-md mx-auto">
|
||||
<form onSubmit={handleSubmit(formSubmit)}>
|
||||
<textarea
|
||||
onChange={(e) => handleTextChange(e)}
|
||||
className="w-full h-32 p-2 border-solid border-2 border-gray-700 rounded-md"
|
||||
ref={register}
|
||||
name="text"
|
||||
/>
|
||||
{!questionIsFromCurrentUser && REVIEW_FEEDBACK.map((item, index) => (
|
||||
<div key={index} className="flex mb-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={item.value}
|
||||
name="feedbackType"
|
||||
ref={register({ required: true })}
|
||||
value={item.value}
|
||||
className="my-auto"
|
||||
defaultChecked={index === 0}
|
||||
/>
|
||||
<label
|
||||
htmlFor={item.value}
|
||||
className="flex flex-col pl-2 w-full"
|
||||
>
|
||||
{item.label}
|
||||
<p className="text-gray-700 text-sm">{item.description}</p>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
<div className="justify-end flex">
|
||||
<Button type="primary" htmlType="submit" className="mt-4" onClick={handleSubmitClick}>
|
||||
{questionIsFromCurrentUser ? 'Responder Parecer' : 'Enviar Parecer'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ReviewMessages'
|
||||
171
app/javascript/pages/question/shared/ViewMode.tsx
Normal file
171
app/javascript/pages/question/shared/ViewMode.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { FC } from "react";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
import { Card } from "../../../components";
|
||||
import { Question } from "../../../__generated__/graphql-schema";
|
||||
import { loadWIRISplugin } from "../../../utils/plugins";
|
||||
import { BLOOM_TAXONOMY, DIFFICULTY } from "../../../utils/types";
|
||||
|
||||
export const ViewModeFragments = gql`
|
||||
fragment QuestionReadOnlyFields on Question {
|
||||
intention
|
||||
instruction
|
||||
support
|
||||
body
|
||||
alternatives {
|
||||
correct
|
||||
text
|
||||
}
|
||||
explanation
|
||||
references
|
||||
authorship
|
||||
authorshipYear
|
||||
difficulty
|
||||
checkType
|
||||
bloomTaxonomy
|
||||
subject {
|
||||
name
|
||||
axis {
|
||||
name
|
||||
}
|
||||
category {
|
||||
name
|
||||
}
|
||||
}
|
||||
status
|
||||
reviewer {
|
||||
id
|
||||
name
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
questionData?: Question
|
||||
}
|
||||
|
||||
export const ViewMode: FC<Props> = ({ questionData: question }) => {
|
||||
if (!question) return null;
|
||||
|
||||
const { alternatives } = question;
|
||||
|
||||
const correctAlternative = alternatives?.find(
|
||||
(alternative) => alternative.correct === true,
|
||||
);
|
||||
|
||||
const incorrectAnswers = alternatives?.filter(
|
||||
(alternative) => alternative.correct === false,
|
||||
);
|
||||
|
||||
function formatDate(stringDate: string) {
|
||||
return new Date(stringDate).toLocaleDateString();
|
||||
}
|
||||
|
||||
const { instruction, support, body } = question;
|
||||
|
||||
const difficulty = DIFFICULTY.find((item) => question.difficulty === item.value)?.label
|
||||
const bloomTaxonomy = BLOOM_TAXONOMY.find((item) => question.bloomTaxonomy === item.value)?.label
|
||||
|
||||
loadWIRISplugin()
|
||||
|
||||
return (
|
||||
<div className="max-w-screen-lg">
|
||||
<Card className="mb-3" title="Características">
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<span className="text-gray-700">Grau de Dificuldade: </span>
|
||||
{difficulty ?? ''}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Habilidade Cognitiva: </span>
|
||||
{bloomTaxonomy ?? ''}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Ano: </span>
|
||||
{question.authorshipYear}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Autoria: </span>
|
||||
{question.authorship === "UNIFESO" ? "Própria" : `Terceiro - ${question.authorship}`}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Atualizada em: </span>
|
||||
{formatDate(question.updatedAt)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Assunto: </span>
|
||||
{question.subject?.name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Categoria: </span>
|
||||
{question.subject?.category?.name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Revisor: </span>
|
||||
{question.reviewer?.name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-700">Eixo de Formação: </span>
|
||||
{question.subject?.axis?.name}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{!!question.intention?.length && (
|
||||
<Card className="mb-3" title="Intenção">
|
||||
<div className="ck-content" dangerouslySetInnerHTML={{ __html: question.intention }} />
|
||||
</Card>
|
||||
)}
|
||||
{instruction && (
|
||||
<Card className="mb-3" title="Instrução">
|
||||
<div className="ck-content" dangerouslySetInnerHTML={{ __html: instruction }} />
|
||||
</Card>
|
||||
)}
|
||||
{support && (
|
||||
<Card className="mb-3" title="Suporte">
|
||||
<div className="ck-content" dangerouslySetInnerHTML={{ __html: support }} />
|
||||
</Card>
|
||||
)}
|
||||
{body && (
|
||||
<Card className="mb-3" title="Enunciado">
|
||||
<div className="ck-content" dangerouslySetInnerHTML={{ __html: body }} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="mb-3" title="Resposta Correta">
|
||||
<div className="ck-content" dangerouslySetInnerHTML={{ __html: correctAlternative?.text ?? '' }} />
|
||||
|
||||
<div className="flex flex-col w-full border border-gray-300 rounded p-4 mt-4 shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-base font-medium mb-3">Explicação</h2>
|
||||
<div
|
||||
className="ck-content ml-2"
|
||||
dangerouslySetInnerHTML={{ __html: question.explanation ?? '' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-gray-400 w-full my-3" style={{ height: "1px" }} />
|
||||
<div>
|
||||
<h2 className="text-base font-medium mb-3">Referências</h2>
|
||||
<div
|
||||
className="ck-content ml-2"
|
||||
dangerouslySetInnerHTML={{ __html: question.references ?? '' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="mb-3" title="Distratores">
|
||||
{incorrectAnswers?.map(({ text }, index) => (
|
||||
<div key={`question-alternative-${index}`}>
|
||||
{index !== 0 && (
|
||||
<div
|
||||
className="bg-gray-400 w-full my-3"
|
||||
style={{ height: "1px" }}
|
||||
/>
|
||||
)}
|
||||
<div className="ck-content" dangerouslySetInnerHTML={{ __html: text ?? '' }} />
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
app/javascript/pages/question/shared/index.ts
Normal file
2
app/javascript/pages/question/shared/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ViewMode";
|
||||
export * from "./ReviewMessages";
|
||||
39
app/javascript/pages/session/Profile.tsx
Normal file
39
app/javascript/pages/session/Profile.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useCurrentUser } from "../../contexts";
|
||||
import { AvatarEditor, Navigator, CurrentUserAvatar } from "../../components";
|
||||
|
||||
export const Profile = () => {
|
||||
const [avatarEditorIsOpen, setAvatarEditorIsOpen] = useState(false);
|
||||
const { user } = useCurrentUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvatarEditor
|
||||
isOpen={avatarEditorIsOpen}
|
||||
setIsOpen={setAvatarEditorIsOpen}
|
||||
/>
|
||||
<Navigator home />
|
||||
<div className="bg-gray-100 w-full my-3">
|
||||
<main>
|
||||
<div className="flex items-center flex-col max-w-4xl m-auto">
|
||||
<div className="bg-white shadow border border-gray-100 flex flex-col items-center rounded p-4 w-full mt-12 mb-4 relative">
|
||||
<div
|
||||
className="w-20 absolute cursor-pointer"
|
||||
style={{ top: "-3.10rem" }}
|
||||
onClick={() => setAvatarEditorIsOpen(true)}
|
||||
>
|
||||
<CurrentUserAvatar />
|
||||
</div>
|
||||
<div className="mt-8 text-center">
|
||||
<h2 className="font-bold">{user?.name || user?.email}</h2>
|
||||
<h2 className="py-4">Centro: CCT</h2>
|
||||
<h2 className="">TODO: Cargo</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
23
app/javascript/pages/session/SignIn.tsx
Normal file
23
app/javascript/pages/session/SignIn.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "../../components";
|
||||
|
||||
export const SignIn = () => {
|
||||
const handleLogin = async () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen bg-primary-normal grid place-items-center">
|
||||
<div>
|
||||
<img
|
||||
alt="Logo do Unifeso"
|
||||
src={'unifesoLogo'}
|
||||
style={{ width: "85%", margin: "auto" }}
|
||||
/>
|
||||
<div className="grid place-items-center">
|
||||
<Button onClick={handleLogin}>Faça login no Google</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
23
app/javascript/pages/session/UnauthorizedAccess.tsx
Normal file
23
app/javascript/pages/session/UnauthorizedAccess.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "../../components";
|
||||
|
||||
export const UnauthorizedAccess = () => {
|
||||
const handleSignOut = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen bg-primary-normal grid place-items-center">
|
||||
<div className="flex flex-col h-32 justify-between">
|
||||
<h1 className="text-white text-xl">
|
||||
{'currentUser?.email'}
|
||||
{" "}
|
||||
não possui permissão para acessar a plataforma!
|
||||
</h1>
|
||||
<div className="grid place-items-center">
|
||||
<Button onClick={handleSignOut}>Encerrar Sessão</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
3
app/javascript/pages/session/index.ts
Normal file
3
app/javascript/pages/session/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { SignIn } from "./SignIn";
|
||||
export { Profile } from "./Profile";
|
||||
export { UnauthorizedAccess } from "./UnauthorizedAccess";
|
||||
11
app/javascript/pages/shared/Loading.tsx
Normal file
11
app/javascript/pages/shared/Loading.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { FC } from "react";
|
||||
|
||||
export const Loading: FC = () => {
|
||||
return (
|
||||
<div className="grid h-screen w-screen bg-primary-dark place-items-center">
|
||||
<div className="text-white text-lg">
|
||||
Carregando...
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
app/javascript/pages/shared/index.ts
Normal file
1
app/javascript/pages/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Loading'
|
||||
Reference in New Issue
Block a user