move move frontend to progress-test

This commit is contained in:
João Geonizeli
2022-07-21 21:16:59 -03:00
parent f8d5d08447
commit 386050d4ad
129 changed files with 159374 additions and 39 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,541 @@
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
/** An ISO 8601-encoded date */
ISO8601Date: any;
/** An ISO 8601-encoded datetime */
ISO8601DateTime: any;
};
export type Axis = {
__typename?: 'Axis';
id: Scalars['ID'];
name: Scalars['String'];
subjects: Array<Subject>;
};
export type Category = {
__typename?: 'Category';
id: Scalars['ID'];
name: Scalars['String'];
subjects: Array<Subject>;
};
/** Autogenerated input type of CreateQuestion */
export type CreateQuestionInput = {
question: QuestionCreateInput;
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
};
/** Autogenerated return type of CreateQuestion */
export type CreateQuestionPayload = {
__typename?: 'CreateQuestionPayload';
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
/** Errors encountered during execution of the mutation. */
errors: Array<Scalars['String']>;
question?: Maybe<Question>;
};
/** Autogenerated input type of CreateReviewMessage */
export type CreateReviewMessageInput = {
message: ReviewMessageInput;
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
};
/** Autogenerated return type of CreateReviewMessage */
export type CreateReviewMessagePayload = {
__typename?: 'CreateReviewMessagePayload';
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
/** Errors encountered during execution of the mutation. */
errors: Array<Scalars['String']>;
reviewMessage?: Maybe<ReviewMessage>;
};
export type DateRangeInput = {
startAt: Scalars['ISO8601Date'];
endAt: Scalars['ISO8601Date'];
};
/** Autogenerated input type of DestroyQuestion */
export type DestroyQuestionInput = {
questionId: Scalars['ID'];
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
};
/** Autogenerated return type of DestroyQuestion */
export type DestroyQuestionPayload = {
__typename?: 'DestroyQuestionPayload';
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
deletedQuestionId?: Maybe<Scalars['ID']>;
/** Errors encountered during execution of the mutation. */
errors: Array<Scalars['String']>;
};
/** Autogenerated input type of FinishQuestion */
export type FinishQuestionInput = {
questionId: Scalars['ID'];
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
};
/** Autogenerated return type of FinishQuestion */
export type FinishQuestionPayload = {
__typename?: 'FinishQuestionPayload';
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
/** Errors encountered during execution of the mutation. */
errors: Array<Scalars['String']>;
question?: Maybe<Question>;
};
export type Mutation = {
__typename?: 'Mutation';
createQuestion?: Maybe<CreateQuestionPayload>;
createReviewMessage?: Maybe<CreateReviewMessagePayload>;
destroyQuestion?: Maybe<DestroyQuestionPayload>;
finishQuestion?: Maybe<FinishQuestionPayload>;
updateQuestion?: Maybe<UpdateQuestionPayload>;
};
export type MutationCreateQuestionArgs = {
input: CreateQuestionInput;
};
export type MutationCreateReviewMessageArgs = {
input: CreateReviewMessageInput;
};
export type MutationDestroyQuestionArgs = {
input: DestroyQuestionInput;
};
export type MutationFinishQuestionArgs = {
input: FinishQuestionInput;
};
export type MutationUpdateQuestionArgs = {
input: UpdateQuestionInput;
};
/** An object with an ID. */
export type Node = {
/** ID of the object. */
id: Scalars['ID'];
};
/** Information about pagination in a connection. */
export type PageInfo = {
__typename?: 'PageInfo';
/** When paginating forwards, the cursor to continue. */
endCursor?: Maybe<Scalars['String']>;
/** When paginating forwards, are there more items? */
hasNextPage: Scalars['Boolean'];
/** When paginating backwards, are there more items? */
hasPreviousPage: Scalars['Boolean'];
/** When paginating backwards, the cursor to continue. */
startCursor?: Maybe<Scalars['String']>;
};
export type Query = {
__typename?: 'Query';
currentUser?: Maybe<User>;
/** Fetches an object given its ID. */
node?: Maybe<Node>;
/** Fetches a list of objects given a list of IDs. */
nodes: Array<Maybe<Node>>;
questions: QuestionConnection;
reviewers: UserConnection;
subjects: SubjectConnection;
};
export type QueryNodeArgs = {
id: Scalars['ID'];
};
export type QueryNodesArgs = {
ids: Array<Scalars['ID']>;
};
export type QueryQuestionsArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
where?: Maybe<QuestionWhereInput>;
};
export type QueryReviewersArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
};
export type QuerySubjectsArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
};
export type Question = Node & {
__typename?: 'Question';
alternatives: Array<QuestionAlternative>;
authorship?: Maybe<Scalars['String']>;
authorshipYear?: Maybe<Scalars['String']>;
bloomTaxonomy?: Maybe<QuestionBloomTaxonomy>;
body?: Maybe<Scalars['String']>;
checkType?: Maybe<QuestionCheckType>;
createdAt: Scalars['ISO8601DateTime'];
difficulty?: Maybe<QuestionDifficulty>;
explanation?: Maybe<Scalars['String']>;
id: Scalars['ID'];
instruction?: Maybe<Scalars['String']>;
intention?: Maybe<Scalars['String']>;
references?: Maybe<Scalars['String']>;
reviewMessages: ReviewMessageConnection;
reviewRequests: Array<ReviewRequest>;
reviewer?: Maybe<User>;
status?: Maybe<QuestionStatus>;
subject?: Maybe<Subject>;
support?: Maybe<Scalars['String']>;
updatedAt: Scalars['ISO8601DateTime'];
user: User;
};
export type QuestionReviewMessagesArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
};
export type QuestionAlternative = {
__typename?: 'QuestionAlternative';
correct: Scalars['Boolean'];
text?: Maybe<Scalars['String']>;
};
export type QuestionAlternativeInput = {
correct?: Maybe<Scalars['Boolean']>;
text?: Maybe<Scalars['String']>;
};
export enum QuestionBloomTaxonomy {
Remember = 'remember',
Understand = 'understand',
Apply = 'apply',
Analyze = 'analyze',
Evaluate = 'evaluate',
Create = 'create'
}
export enum QuestionCheckType {
UniqueAnswer = 'unique_answer',
IncompleteAffirmation = 'incomplete_affirmation',
MultipleAnswer = 'multiple_answer',
NegativeFocus = 'negative_focus',
AssertionAndReason = 'assertion_and_reason',
Gap = 'gap',
Interpretation = 'interpretation',
Association = 'association',
OrderingOrRanking = 'ordering_or_ranking',
ConstantAlternatives = 'constant_alternatives'
}
/** The connection type for Question. */
export type QuestionConnection = {
__typename?: 'QuestionConnection';
/** A list of edges. */
edges: Array<QuestionEdge>;
/** A list of nodes. */
nodes: Array<Question>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
totalCount: Scalars['Int'];
};
export type QuestionCreateInput = {
instruction: Scalars['String'];
support: Scalars['String'];
body: Scalars['String'];
alternatives: Array<QuestionAlternativeInput>;
explanation: Scalars['String'];
references: Scalars['String'];
authorshipYear: Scalars['String'];
authorship: Scalars['String'];
intention?: Maybe<Scalars['String']>;
status: QuestionStatus;
checkType?: Maybe<QuestionCheckType>;
difficulty?: Maybe<QuestionDifficulty>;
bloomTaxonomy?: Maybe<QuestionBloomTaxonomy>;
subjectId?: Maybe<Scalars['ID']>;
reviewerUserId?: Maybe<Scalars['ID']>;
};
export enum QuestionDifficulty {
Easy = 'easy',
Medium = 'medium',
Hard = 'hard'
}
/** An edge in a connection. */
export type QuestionEdge = {
__typename?: 'QuestionEdge';
/** A cursor for use in pagination. */
cursor: Scalars['String'];
/** The item at the end of the edge. */
node?: Maybe<Question>;
};
export enum QuestionStatus {
Draft = 'DRAFT',
WaitingReview = 'WAITING_REVIEW',
WithRequestedChanges = 'WITH_REQUESTED_CHANGES',
Approved = 'APPROVED',
Registered = 'REGISTERED'
}
export type QuestionUpdateInput = {
instruction: Scalars['String'];
support: Scalars['String'];
body: Scalars['String'];
alternatives: Array<QuestionAlternativeInput>;
explanation: Scalars['String'];
references: Scalars['String'];
authorshipYear: Scalars['String'];
authorship: Scalars['String'];
intention?: Maybe<Scalars['String']>;
status: QuestionStatus;
checkType?: Maybe<QuestionCheckType>;
difficulty?: Maybe<QuestionDifficulty>;
bloomTaxonomy?: Maybe<QuestionBloomTaxonomy>;
subjectId?: Maybe<Scalars['ID']>;
reviewerUserId?: Maybe<Scalars['ID']>;
id: Scalars['ID'];
};
export type QuestionWhereInput = {
checkType?: Maybe<Array<QuestionCheckType>>;
status?: Maybe<Array<QuestionStatus>>;
difficulty?: Maybe<Array<QuestionDifficulty>>;
bloomTaxonomy?: Maybe<Array<QuestionBloomTaxonomy>>;
authorshipYear?: Maybe<Array<Scalars['String']>>;
subjectId?: Maybe<Scalars['ID']>;
userId?: Maybe<Scalars['ID']>;
createDate?: Maybe<DateRangeInput>;
unifesoAuthorship?: Maybe<Scalars['Boolean']>;
};
export type ReviewMessage = {
__typename?: 'ReviewMessage';
createdAt: Scalars['ISO8601DateTime'];
feedbackType: ReviewMessageFeedbackType;
id: Scalars['ID'];
question: Question;
text: Scalars['String'];
updatedAt: Scalars['ISO8601DateTime'];
user: User;
};
/** The connection type for ReviewMessage. */
export type ReviewMessageConnection = {
__typename?: 'ReviewMessageConnection';
/** A list of edges. */
edges: Array<ReviewMessageEdge>;
/** A list of nodes. */
nodes: Array<ReviewMessage>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
totalCount: Scalars['Int'];
};
/** An edge in a connection. */
export type ReviewMessageEdge = {
__typename?: 'ReviewMessageEdge';
/** A cursor for use in pagination. */
cursor: Scalars['String'];
/** The item at the end of the edge. */
node?: Maybe<ReviewMessage>;
};
export enum ReviewMessageFeedbackType {
RequestChanges = 'REQUEST_CHANGES',
Approve = 'APPROVE',
Answer = 'ANSWER'
}
export type ReviewMessageInput = {
feedbackType: ReviewMessageFeedbackType;
text: Scalars['String'];
questionId: Scalars['ID'];
};
export type ReviewRequest = {
__typename?: 'ReviewRequest';
answered: Scalars['Boolean'];
id: Scalars['ID'];
question: Question;
user: User;
};
/** The connection type for ReviewRequest. */
export type ReviewRequestConnection = {
__typename?: 'ReviewRequestConnection';
/** A list of edges. */
edges: Array<ReviewRequestEdge>;
/** A list of nodes. */
nodes: Array<ReviewRequest>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
totalCount: Scalars['Int'];
};
/** An edge in a connection. */
export type ReviewRequestEdge = {
__typename?: 'ReviewRequestEdge';
/** A cursor for use in pagination. */
cursor: Scalars['String'];
/** The item at the end of the edge. */
node?: Maybe<ReviewRequest>;
};
export type Subject = {
__typename?: 'Subject';
axis: Axis;
category: Category;
id: Scalars['ID'];
name: Scalars['String'];
questions: QuestionConnection;
};
export type SubjectQuestionsArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
where?: Maybe<QuestionWhereInput>;
};
/** The connection type for Subject. */
export type SubjectConnection = {
__typename?: 'SubjectConnection';
/** A list of edges. */
edges: Array<SubjectEdge>;
/** A list of nodes. */
nodes: Array<Subject>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
totalCount: Scalars['Int'];
};
/** An edge in a connection. */
export type SubjectEdge = {
__typename?: 'SubjectEdge';
/** A cursor for use in pagination. */
cursor: Scalars['String'];
/** The item at the end of the edge. */
node?: Maybe<Subject>;
};
/** Autogenerated input type of UpdateQuestion */
export type UpdateQuestionInput = {
question: QuestionUpdateInput;
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
};
/** Autogenerated return type of UpdateQuestion */
export type UpdateQuestionPayload = {
__typename?: 'UpdateQuestionPayload';
/** A unique identifier for the client performing the mutation. */
clientMutationId?: Maybe<Scalars['String']>;
/** Errors encountered during execution of the mutation. */
errors: Array<Scalars['String']>;
question?: Maybe<Question>;
};
export type User = {
__typename?: 'User';
activeReviewRequests: ReviewRequestConnection;
avatarUrl?: Maybe<Scalars['String']>;
email: Scalars['String'];
id: Scalars['ID'];
inactiveReviewRequests: ReviewRequestConnection;
name: Scalars['String'];
roles: Array<UserRole>;
};
export type UserActiveReviewRequestsArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
};
export type UserInactiveReviewRequestsArgs = {
after?: Maybe<Scalars['String']>;
before?: Maybe<Scalars['String']>;
first?: Maybe<Scalars['Int']>;
last?: Maybe<Scalars['Int']>;
};
/** The connection type for User. */
export type UserConnection = {
__typename?: 'UserConnection';
/** A list of edges. */
edges: Array<UserEdge>;
/** A list of nodes. */
nodes: Array<User>;
/** Information to aid in pagination. */
pageInfo: PageInfo;
totalCount: Scalars['Int'];
};
/** An edge in a connection. */
export type UserEdge = {
__typename?: 'UserEdge';
/** A cursor for use in pagination. */
cursor: Scalars['String'];
/** The item at the end of the edge. */
node?: Maybe<User>;
};
export enum UserRole {
Admin = 'admin',
Teacher = 'teacher',
Nde = 'nde',
Coordinator = 'coordinator',
CenterDirector = 'center_director',
ProRector = 'pro_rector'
}

View File

@@ -1,14 +1,26 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { ApolloContext } from "./contexts/ApolloContext";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
const App = () => {
import { Appbar } from "./components";
import { ApolloContext } from "./contexts";
import { PrivateRoutes } from "./routes";
import { store } from "./services/store";
export const App = () => {
return (
<ApolloContext>
<div>Hello, Rails 7!</div>
<Provider store={store}>
<BrowserRouter>
<Appbar />
<PrivateRoutes />
</BrowserRouter>
</Provider>
</ApolloContext>
);
};
}
const container = document.getElementById("app");

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,18 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="500px" height="181px" viewBox="0 0 5000 1810" preserveAspectRatio="xMidYMid meet">
<g id="layer101" fill="#ffffff" stroke="none">
<path d="M293 1203 c161 -196 297 -358 303 -360 5 -2 20 17 32 42 l22 45 -202 315 -203 315 -122 0 -123 0 293 -357z"/>
<path d="M280 1554 c0 -7 262 -417 351 -550 35 -52 72 -90 135 -139 96 -74 88 -74 133 9 l23 41 -150 320 -149 320 -172 3 c-94 1 -171 0 -171 -4z"/>
<path d="M650 1555 c0 -17 305 -654 328 -685 l27 -37 45 21 c37 18 45 26 43 46 -2 23 -30 234 -68 520 -8 58 -15 113 -15 123 0 16 -15 17 -180 17 -99 0 -180 -2 -180 -5z"/>
<path d="M1030 1548 c0 -7 20 -164 45 -348 l45 -336 124 -298 c120 -287 132 -314 139 -305 2 2 22 69 44 149 41 143 193 673 278 965 24 83 47 158 50 168 7 16 -14 17 -359 17 -286 0 -366 -3 -366 -12z"/>
<path d="M1970 1551 c-79 -17 -126 -57 -162 -136 -20 -46 -22 -68 -26 -287 -2 -131 -1 -249 2 -263 6 -24 10 -25 81 -25 l75 0 0 231 c0 193 3 237 16 265 23 48 59 64 143 64 63 0 74 -3 101 -27 44 -39 49 -78 50 -320 l0 -213 75 0 75 0 0 259 c0 279 -5 311 -55 377 -43 56 -103 78 -230 80 -60 2 -126 -1 -145 -5z"/>
<path d="M2386 1545 c-11 -30 -6 -92 11 -131 13 -31 17 -83 21 -254 5 -242 6 -244 81 -288 41 -24 58 -27 161 -31 187 -6 260 23 313 123 21 39 22 52 22 316 l0 275 -69 3 c-49 2 -72 -1 -77 -10 -5 -7 -9 -119 -9 -249 l0 -237 -28 -30 c-55 -59 -199 -52 -246 11 -20 26 -21 45 -26 270 l-5 242 -71 3 c-59 2 -73 0 -78 -13z"/>
<path d="M3018 1303 c2 -211 0 -267 -13 -305 -15 -48 -20 -124 -9 -152 9 -23 159 -23 168 0 3 9 6 167 6 353 0 249 -3 340 -12 349 -7 7 -40 12 -77 12 l-66 0 3 -257z"/>
<path d="M3197 1553 c-4 -3 -7 -197 -7 -430 0 -464 0 -466 59 -516 37 -31 79 -40 181 -41 l75 -1 0 65 0 65 -60 3 c-32 2 -65 8 -72 14 -8 6 -13 33 -13 64 l0 53 73 3 c66 3 72 5 75 25 2 14 -9 38 -29 63 -29 36 -37 40 -76 40 l-43 0 0 284 c0 156 -3 291 -6 300 -5 13 -22 16 -78 16 -40 0 -76 -3 -79 -7z"/>
<path d="M3625 1546 c-92 -29 -139 -70 -177 -151 -21 -45 -23 -65 -23 -195 0 -103 4 -155 14 -180 26 -63 72 -117 124 -146 48 -27 60 -29 157 -29 121 0 160 15 222 83 53 59 73 121 73 227 l0 90 -217 3 -218 2 0 38 c0 20 9 51 20 69 34 57 62 67 190 73 l115 5 0 60 0 60 -120 2 c-75 1 -135 -3 -160 -11z m235 -437 c0 -45 -29 -98 -64 -120 -39 -24 -108 -25 -148 -1 -35 20 -68 79 -68 122 l0 30 140 0 140 0 0 -31z"/>
<path d="M3975 1537 c-4 -13 -5 -41 -3 -63 l3 -39 155 -5 c85 -3 161 -9 168 -13 6 -5 12 -26 12 -47 0 -39 -1 -40 -96 -97 -170 -103 -170 -103 -178 -169 -4 -31 -16 -78 -27 -103 l-20 -45 29 -35 c46 -56 103 -81 197 -84 l80 -2 0 65 0 65 -68 3 c-58 3 -69 6 -83 27 -24 37 -7 54 128 135 68 41 137 88 155 105 85 84 52 239 -64 296 -44 22 -63 24 -215 27 l-167 4 -6 -25z"/>
<path d="M4573 1546 c-47 -15 -113 -54 -113 -66 0 -4 7 -24 15 -43 19 -45 19 -117 1 -160 -7 -18 -33 -50 -56 -70 l-43 -36 6 -62 c12 -112 74 -206 165 -251 66 -32 215 -33 283 -2 58 27 109 78 140 140 22 46 24 60 24 204 0 143 -2 159 -23 202 -28 55 -85 111 -139 134 -52 22 -205 28 -260 10z m198 -159 c41 -28 57 -75 58 -174 2 -156 -35 -222 -126 -230 -58 -5 -103 19 -130 69 -23 43 -25 235 -4 287 30 69 133 94 202 48z"/>
<path d="M2994 717 c-3 -8 -4 -43 -2 -78 l3 -64 85 0 85 0 0 75 0 75 -83 3 c-63 2 -84 0 -88 -11z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,333 @@
/*
* CKEditor 5 (v26.0.0) content styles.
* Generated on Sun, 14 Mar 2021 13:52:40 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-todo-list-checkmark-size: 16px;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
/* ckeditor5-html-embed/theme/htmlembed.css */
.ck-content .raw-html-embed {
margin: 1em auto;
min-width: 15em;
font-style: normal;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
/* margin: 1em auto; */
}
/* ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 1em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li {
margin-bottom: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li .todo-list {
margin-top: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
padding: 0;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
display: none;
}
}

View File

@@ -0,0 +1,9 @@
.ck-editor {
height: 100% !important;
}
body, #root {
height: 100vh;
background-color: #f7fafc;
font-family: Roboto, 'Times New Roman', Times, serif;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View File

@@ -0,0 +1,15 @@
import React, { FC } from "react";
type Props = {
children: any
}
export const Alert: FC<Props> = ({ children }) => (
<div
className="w-full md:my-2 p-2 bg-red-600 items-center text-red-100 leading-none lg:rounded flex lg:inline-flex"
role="alert"
>
<span className="flex px-2 py-1 font-bold">Oops!</span>
<span className="font-semibold mr-2 text-left flex-auto">{children}</span>
</div>
);

View File

@@ -0,0 +1 @@
export { Alert } from "./Alert";

View File

@@ -0,0 +1,38 @@
import React, { FC } from 'react'
import { MdDone, MdError, MdInfo, MdWarning } from 'react-icons/md'
type AlertSeverity = 'error' | 'warning' | 'info' | 'success'
const ICONS = {
error: <MdError />,
warning: <MdWarning />,
info: <MdInfo />,
success: <MdDone />
}
const COLOR_CLASSES = {
error: 'bg-red-300 border-red-600 text-red-800',
warning: 'bg-orange-300 border-orange-600 text-orange-800',
info: 'bg-blue-300 border-blue-600 text-blue-800',
success: 'bg-green-300 border-green-600 text-green-800',
}
export type Props = {
severity?: AlertSeverity
text?: string
}
export const AlertV2: FC<Props> = ({
severity = 'info',
text = '',
}) => (
<div className={`flex rounded shadow p-4 mx-auto my-2 ${COLOR_CLASSES[severity]}`}>
<div className="text-xl my-auto pr-2">
{ICONS[severity]}
</div>
<span>
{text}
</span>
</div>
)

View File

@@ -0,0 +1,2 @@
export { AlertV2 } from "./AlertV2";
export type { Props as AlertV2Props } from "./AlertV2";

View File

@@ -0,0 +1,221 @@
import React, { FC, Fragment, useState } from 'react'
import { useHistory, useLocation } from 'react-router';
import { Menu, Transition } from '@headlessui/react'
import { ChartBarIcon, ClipboardListIcon } from '@heroicons/react/outline'
import { Dialog } from '../Dialog'
import { useDispatch, useSelector } from 'react-redux';
import { useCurrentUser } from '../../contexts';
import { RootState } from '../../services/store';
import { classNames } from '../../utils';
import { DashboardRoutePaths, QuestionRoutePaths, SessionRoutePaths } from '../../routes'
import { turnOff } from '../../services/store/unsavedChanges';
import { CurrentUserAvatar } from "../CurrentUserAvatar";
const UserMenu: FC = () => {
const { user } = useCurrentUser();
const history = useHistory();
const [confirmLogout, setConfirmLogout] = useState(false)
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
const dispatch = useDispatch()
const doLogout = () => {
setConfirmLogout(false)
dispatch(turnOff())
history.push('/')
}
const handleLogout = () => {
if (unsavedChanges && !confirmLogout) {
setConfirmLogout(true)
} else {
doLogout()
}
}
const [newPath, setNewPath] = useState<string>()
const handleForcedRedirect = () => {
if (!newPath) return
dispatch(turnOff())
setNewPath(undefined)
history.push(newPath)
}
const handleLinkClick = (pathname: string) => {
if (unsavedChanges) {
setNewPath(pathname)
} else {
history.push(pathname)
}
}
const menuItems = [
{
onClick: () => { handleLinkClick(SessionRoutePaths.show) },
label: 'Perfil'
},
{
onClick: handleLogout,
label: 'Sair'
}
]
return (
<>
<Dialog
isOpen={!!newPath}
setIsOpen={(value) => setNewPath(value ? newPath : undefined)}
onConfirmation={handleForcedRedirect}
title="Modificações não Salvas"
text="Todas as alterações serão descartadas. Deseja continuar?"
/>
<Dialog
isOpen={confirmLogout}
setIsOpen={setConfirmLogout}
onConfirmation={handleLogout}
title="Modificações não Salvas"
text="Todas as alterações serão descartadas. Deseja continuar?"
/>
<Menu as="div" className="relative h-full">
{({ open }) => (
<>
<Menu.Button
className="h-full flex flex-row px-2 items-center hover:bg-primary-dark text-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
>
<span className="hidden md:block pr-2">
{user?.name}
</span>
<div className="w-12">
<CurrentUserAvatar />
</div>
</Menu.Button>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="z-50 origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none cursor-pointer"
>
{menuItems.map((item) => (
<Menu.Item key={`menu-item-${item.label}`} onClick={item.onClick}>
{({ active }) => (
<span
className={classNames(
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-900'
)}
>
{item.label}
</span>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</>
)}
</Menu>
</>
)
}
const Links: FC = () => {
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
const dispatch = useDispatch()
const location = useLocation()
const history = useHistory()
const [newPath, setNewPath] = useState<string>()
const handleForcedRedirect = () => {
if (!newPath) return
dispatch(turnOff())
setNewPath(undefined)
history.push(newPath)
}
const handleLinkClick = (pathname: string) => {
if (unsavedChanges) {
setNewPath(pathname)
} else {
history.push(pathname)
}
}
const links = [{
icon: <ChartBarIcon className="w-6" />,
tabel: 'Painel',
pathname: DashboardRoutePaths.index,
isCurrent: location.pathname.includes('dashboard'),
},
{
icon: <ClipboardListIcon className="w-6" />,
tabel: 'Edição',
pathname: QuestionRoutePaths.index,
isCurrent: location.pathname.includes('question'),
}]
return (
<>
<Dialog
isOpen={!!newPath}
setIsOpen={(value) => setNewPath(value ? newPath : undefined)}
onConfirmation={handleForcedRedirect}
title="Modificações não Salvas"
text="Todas as alterações serão descartadas. Deseja continuar?"
/>
<div className="h-full flex items-center pl-4">
{links.map((link) => (
<button
className={`h-full flex items-center px-2 mx-2 text-gray-300 hover:bg-primary-dark ${link.isCurrent ? 'underline bg-primary-dark' : ''}`}
key={`navbar-link-${link.pathname}`}
onClick={() => handleLinkClick(link.pathname)}
>
<span className="pr-2 ">
{link.icon}
</span>
{link.tabel}
</button>
))}
</div>
</>
)
}
const Logo: FC = () => (
<div className="h-full grid place-items-center">
<img
alt="Símbolo do Unifeso"
className="hidden md:block h-12 w-auto"
src={'unifesoLogo'}
/>
<img
alt="Logotipo do Unifeso"
className="md:hidden h-12 w-auto"
src={'unifesoLogoCompact'}
/>
</div>
)
export const Appbar = () => {
return (
<div className="px-4 bg-primary-normal flex items-center justify-between h-16 shadow-md">
<div className="flex h-full">
<Logo />
<Links />
</div>
<UserMenu />
</div>
)
}

View File

@@ -0,0 +1 @@
export * from "./Appbar";

View File

@@ -0,0 +1,63 @@
import React, { FC, useState } from "react";
import axios from "axios";
import { Alert } from "../Alert";
import { Button } from "../Button";
import { PhotoCrop } from "./PhotoCrop";
import { useCurrentUser } from "../../contexts";
import { Modal } from "../Modal";
type Props = {
isOpen: boolean
setIsOpen: (value: boolean) => void;
};
export const AvatarEditor: FC<Props> = ({ isOpen, setIsOpen }) => {
const [croppedImage, setCroppedImage] = useState<any>()
const [alert, setAlert] = useState<boolean>()
const { refetch, authToken } = useCurrentUser()
const instance = axios.create({
});
instance.defaults.headers.common.Authorization = `Bearer ${authToken}`;
const onSubmit = () => {
instance
.post("/update_avatar", {
upload: croppedImage,
})
.then((res) => {
if (res.status === 200) {
setIsOpen(false)
refetch()
} else {
setAlert(true);
}
})
.catch(() => {
setAlert(true);
});
};
return (
<Modal
title="Alterar Imagem de Perfil"
isOpen={isOpen}
setIsOpen={setIsOpen}
buttons={
<>
<Button onClick={() => setIsOpen(false)}>
Cancelar
</Button>
<Button type="primary" onClick={() => onSubmit()}>
Salvar
</Button>
</>
}
>
{alert && <Alert>Algo deu errado, tente novamente mais tarde.</Alert>}
<PhotoCrop callback={setCroppedImage} />
</Modal>
);
};

View File

@@ -0,0 +1,49 @@
import React, { FC, useState } from "react";
import PhotoCropper from "react-avatar-edit";
type Props = {
callback: (value: any) => void
}
const borderStyle: React.CSSProperties = {
textAlign: 'center',
margin: 'auto',
borderStyle: 'dotted',
borderWidth: '0.3rem',
borderRadius: '0.3rem',
}
export const PhotoCrop: FC<Props> = ({ callback }) => {
const [result, setResult] = useState<any>();
const onCrop = (cropped: any) => {
setResult(cropped);
callback(result);
};
const onClose = () => {
setResult(null);
};
const onBeforeFileLoad = (elem: any) => {
if (elem.target.files[0].size > 1000000) {
elem.target.value = "";
alert("A imagem selecionada é grande de mais!")
}
};
const dimention = 300;
return (
<PhotoCropper
borderStyle={borderStyle}
label="Escolha uma imagem"
width={dimention}
height={dimention}
imageWidth={dimention}
imageHeight={dimention}
onCrop={(e) => onCrop(e)}
onClose={() => onClose()}
onBeforeFileLoad={onBeforeFileLoad}
/>
);
};

View File

@@ -0,0 +1 @@
export { AvatarEditor } from "./AvatarEditor";

View File

@@ -0,0 +1,63 @@
import React from 'react'
const styleClasses = {
primary: "bg-primary-normal hover:bg-primary-dark text-white",
secondary: "bg-gray-200 hover:bg-gray-400 text-gray-800",
disabled: "bg-gray-200 text-gray-600 cursor-not-allowed shadow-none hover:shadow-none",
tertiary: "shadow-none hover:shadow-none drop-shadow-sm text-gray-900 hover:text-gray-600",
}
export type ButtonProps = {
type?: 'default' | 'primary' | 'tertiary';
className?: string;
children?: string | JSX.Element;
htmlType?: 'submit' | 'button' | 'reset';
onClick?: React.MouseEventHandler<HTMLElement>;
disabled?: boolean
}
const ButtonBase: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
const {
type = 'default',
className = '',
htmlType = 'button',
onClick,
disabled,
children,
...rest
} = props
const buttonRef = (ref as any) || React.createRef<HTMLElement>()
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
if (htmlType !== 'submit') {
e.preventDefault()
}
if (disabled || !onClick) return
onClick(e)
}
const extraClasses = () => {
if (disabled) return styleClasses.disabled
if (type === 'primary') return styleClasses.primary
if (type === 'tertiary') return styleClasses.tertiary
return styleClasses.secondary
}
return <button
{...rest}
type={htmlType}
disabled={disabled}
className={`transition duration-300 ease-in-out block text-center cursor-pointer p-2 px-8 rounded shadow-lg hover:shadow-lg ${extraClasses()} ${className}`}
onClick={handleClick}
ref={buttonRef}
>
{children}
</button>
}
export const Button = React.forwardRef<unknown, ButtonProps>(ButtonBase)

View File

@@ -0,0 +1 @@
export { Button } from "./Button";

View File

@@ -0,0 +1,23 @@
import React, { FC } from "react";
type Props = {
title: string
action?: () => void
children: any
className?: string
}
export const Card: FC<Props> = ({
title, action, children, className = '',
}) => (
<div className={`bg-white md:rounded shadow-sm border border-gray-300 w-full ${className}`}>
<div className="border-b border-gray-300 bg-gray-100 md:rounded-t p-2 shadow-sm flex items-center">
<span className="text-lg text-gray-800 flex-grow">{title}</span>
{
action ? action() : null
}
</div>
<div className="p-4 h-full">
{children}
</div>
</div>
);

View File

@@ -0,0 +1 @@
export { Card } from "./Card";

View File

@@ -0,0 +1,19 @@
import React, { FC } from "react";
import styled from "styled-components";
const Grid = styled.div`
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
`;
type Props = {
className?: string
children: any
}
export const CardGrid: FC<Props> = ({ children, className }) => (
<Grid className={className}>
{children}
</Grid>
);

View File

@@ -0,0 +1 @@
export { CardGrid } from "./CardGrid";

View File

@@ -0,0 +1,14 @@
import React, {FC} from 'react'
import {useCurrentUser} from "../../contexts";
import {UserAvatar} from "../UserAvatar";
export const CurrentUserAvatar: FC = () => {
const {user} = useCurrentUser()
if (!user) return null
return (
<UserAvatar user={user}/>
)
}

View File

@@ -0,0 +1 @@
export * from './CurrentUserAvatar'

View File

@@ -0,0 +1,46 @@
import React, { FC } from "react"
import { Button } from "../Button"
import { Modal } from '../Modal'
type DialogType = 'confirmation' | 'notice'
type Props = {
type?: DialogType
title: string
isOpen?: boolean
text?: any
setIsOpen: (state: boolean) => void
onConfirmation: () => void
};
export const Dialog: FC<Props> = ({
type = 'confirmation',
title,
isOpen: open = false,
setIsOpen,
onConfirmation,
text,
}) => {
return (
<Modal
title={title}
isOpen={open}
setIsOpen={setIsOpen}
buttons={
<>
{type === 'confirmation' &&
<Button onClick={() => setIsOpen(false)}>
Cancelar
</Button>
}
<Button type="primary" onClick={onConfirmation}>
Confirmar
</Button>
</>
}
>
{text}
</Modal>
)
};

View File

@@ -0,0 +1 @@
export * from "./Dialog";

View File

@@ -0,0 +1,62 @@
import React, { FC, Fragment } from 'react'
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/outline'
type Item = {
title?: string | JSX.Element
body?: string | JSX.Element
icon?: JSX.Element
}
type Props = {
items: Item[]
}
export const Disclosures: FC<Props> = ({
items
}) => {
return (
<div className="w-full grid grid-cols-1 gap-3">
<div className="w-full max-w-md p-2 mx-auto bg-white rounded-2xl">
{items.map((item) => (
<Disclosure as="div" className="my-2 rounded border">
{({ open }) => (
<>
<Disclosure.Button as={Fragment}>
<button className="flex p-2 bg-gray-200 w-full justify-between">
<div className="grid place-items-center">
{item.icon}
</div>
<span className="pl-2">
{item.title}
</span>
<ChevronDownIcon
className={`${open ? 'transform rotate-180' : ''} w-5 h-5 text-gray-800`}
/>
</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-2 bg-gray-100"
>
{item.body ?? 'Nenhum comentário.'}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
)
)}
</div>
</div>
)
}

View File

@@ -0,0 +1 @@
export * from "./Disclosures";

View File

@@ -0,0 +1,39 @@
import React, {ChangeEvent} from 'react'
import {v4 as uuid} from 'uuid'
export type Props = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
label?: string
}
const ButtonBase: React.ForwardRefRenderFunction<unknown, Props> = (props, ref) => {
const {
className = '',
onChange,
label,
...rest
} = props
const inputRef = (ref as any) || React.createRef<HTMLElement>()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!onChange) return
onChange(e)
}
const id = uuid()
return (
<span>
{label ?? <label htmlFor={id}>{label}</label>}
<input
{...rest}
id={id}
className={`block rounded p-1 w-full border-gray-400 border shadow-sm ${className}`}
onChange={handleChange}
ref={inputRef}
/>
</span>
)
}
export const Input = React.forwardRef<unknown, Props>(ButtonBase)

View File

@@ -0,0 +1 @@
export * from './Input'

View File

@@ -0,0 +1,19 @@
import React, { FC } from "react";
import styled from "styled-components";
const StyledInputGroup = styled.div`
&:first-of-type {
margin-top: 0
}
&:last-of-type {
margin-bottom: 0
}
`;
type Props = {
children?: any
}
export const InputGroup: FC<Props> = ({ children }) => (
<StyledInputGroup className="mt-4 mb-2">{children}</StyledInputGroup>
);

View File

@@ -0,0 +1 @@
export { InputGroup } from "./InputGroup";

View File

@@ -0,0 +1,57 @@
import React, { FC } from 'react'
type ListItemIconProps = {
icon: JSX.Element
}
const ListItemIcon: FC<ListItemIconProps> = ({ icon }) => {
return (
<div className="grid place-items-center pr-3">
{icon}
</div>
)
}
type ListItemTextProps = {
text?: string
}
const ListItemText: FC<ListItemTextProps> = ({ text }) => {
return (
<div className="pl-2">
<p>
{text ?? ''}
</p>
</div>
)
}
type ListItemProps = {
icon?: JSX.Element
text?: string
}
export const ListItem: FC<ListItemProps> = ({ icon, text, children }) => {
return (
<li className="flex py-2 border-t border-b border-gray-200">
{icon && <ListItemIcon icon={icon} />}
{text && <ListItemText text={text} />}
{children}
</li>
)
}
type ListProps = {
className?: string
}
export const List: FC<ListProps> = ({
className = '',
children,
}) => {
return (
<ul className={`list-none p-0 m-0 ${className}`}>
{children}
</ul>
)
}

View File

@@ -0,0 +1 @@
export * from "./List";

View File

@@ -0,0 +1,82 @@
import React, { FC, Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
type Props = {
isOpen: boolean
setIsOpen: (state: boolean) => void
buttons?: any,
title: string,
className?: string,
}
export const Modal: FC<Props> = ({
isOpen,
setIsOpen,
children,
buttons,
title,
className = '',
}) => {
const closeModal = () => {
setIsOpen(false)
}
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog
open={isOpen}
as="div"
className={`fixed inset-0 z-10 overflow-y-auto ${className}`}
onClose={closeModal}
>
<div className="min-h-screen px-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-900 bg-opacity-50" />
</Transition.Child>
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 mt-2 mb-4"
>
{title}
</Dialog.Title>
<div className="mt-2">
{children}
</div>
{buttons &&
<div className="mt-4 grid grid-flow-col gap-3">
{buttons}
</div>
}
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
)
}

View File

@@ -0,0 +1 @@
export * from "./Modal";

View File

@@ -0,0 +1,106 @@
import React, { FC, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useHistory } from "react-router-dom";
import { FaHome, FaPlus } from "react-icons/fa";
import styled from "styled-components";
import { turnOff } from "../../services/store/unsavedChanges";
import { RootState } from "../../services/store";
import { Dialog } from "../Dialog";
import {QuestionRoutePaths} from "../../routes";
const HorizontalMenu = styled.ul`
margin: 0;
padding: 0;
list-style: none;
width: 100%;
display: flex;
& > li {
display: inline;
cursor: pointer;
}
& > li {
display: inline;
}
& > li > div {
cursor: pointer;
display: inline-flex;
flex-direction: row;
margin-right: 2rem;
}
`;
type ItemProps = {
className?: string
children?: any
}
const Item: FC<ItemProps> = ({ children, className }) => (
<div className={`hover:text-white ${className || ""}`}>
{children}
</div>
);
type Props = {
home?: boolean
newQuestion?: boolean
children?: any
}
export const Navigator: FC<Props> = ({
home = false, newQuestion = false, children,
}) => {
const [confirmLeaveDialog, setConfirmLeaveDialog] = useState(false);
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
const dispatch = useDispatch()
const history = useHistory();
const confirmLeave = () => {
dispatch(turnOff());
history.push(QuestionRoutePaths.index);
};
const goHome = () => {
if (unsavedChanges) {
setConfirmLeaveDialog(true);
} else {
confirmLeave();
}
};
return (
<>
<Dialog
isOpen={confirmLeaveDialog}
setIsOpen={(value) => setConfirmLeaveDialog(value)}
onConfirmation={confirmLeave}
title="Modificações não Salvas"
text="Todas as alterações serão descartadas. Deseja continuar?"
/>
<div className="flex p-1 text-md px-2 sm:px-8 text-gray-400 bg-primary-dark shadow-md" style={{ maxHeight: "34.4px" }}>
<HorizontalMenu className="list-none">
{home
&& (
<Item>
<button onClick={() => goHome()} className="flex">
<FaHome className="my-auto" />
<span className="pl-3">Início</span>
</button>
</Item>
)}
{
(newQuestion) ? (
<Item>
<Link to="/questions/new" className="flex">
<FaPlus className="my-auto" />
<span className="pl-3">Nova Questão</span>
</Link>
</Item>
) : null
}
{children}
</HorizontalMenu>
</div>
</>
);
};

View File

@@ -0,0 +1 @@
export { Navigator } from "./Navegator";

View File

@@ -0,0 +1,27 @@
import React, {FC} from "react";
import {User} from "../../__generated__/graphql-schema";
import BoringAvatar from "boring-avatars";
type Props = {
user: User
className?: string
}
export const UserAvatar: FC<Props> = ({user, className}) => {
return (
<div className={`rounded-full border-2 border-primary-light shadow ${className || ''}`}>
{user.avatarUrl ?
<img
src={`${process.env.REACT_APP_BACKEND_URL}/${user.avatarUrl}`}
alt={`Avatar do usuário ${user.name}`}
/>
: <BoringAvatar
size={"100%"}
name={user.name}
variant="pixel"
colors={["#595F72", "#575D90", "#84A07C", "#C3D350", "#E6F14A"]}
/>
}
</div>
)
};

View File

@@ -0,0 +1 @@
export { UserAvatar } from "./UserAvatar";

View File

@@ -0,0 +1,16 @@
export * from "./Alert";
export * from "./AlertV2";
export * from "./Button";
export * from "./Card";
export * from "./CardGrid";
export * from "./InputGroup";
export * from "./AvatarEditor";
export * from "./Navegator";
export * from "./UserAvatar";
export * from "./Dialog";
export * from "./Appbar";
export * from "./Modal";
export * from "./List";
export * from "./Disclosures";
export * from './CurrentUserAvatar'
export * from './Input'

View File

@@ -0,0 +1,75 @@
import React, {
createContext, useContext, useState, FC
} from "react";
import { useQuery, gql } from "@apollo/client";
import { Query, UserRole } from "../__generated__/graphql-schema";
import { UnauthorizedAccess } from "../pages/session";
import { Loading } from "../pages/shared";
export type UserContext = {
user?: Query['currentUser']
refetch: () => void
isOnlyTeacher: boolean
authToken: string
}
const Context = createContext<UserContext>({
refetch: () => {
},
isOnlyTeacher: false,
authToken: ''
})
export const useCurrentUser = (): UserContext => {
const context = useContext(Context);
if (context === null) {
throw new Error("You probably forgot to put <UserContext>.");
}
return context;
};
const CurrentUserQuery = gql`
query CurrentUserQuery {
currentUser {
id
name
email
avatarUrl
roles
}
}
`;
type Props = {
children: any
authToken: string
}
export const UserContext: FC<Props> = ({ children, authToken }) => {
const [user, setUser] = useState<Query['currentUser']>();
const isOnlyTeacher = !!(user?.roles.includes(UserRole.Teacher) && user?.roles.length === 1)
const { refetch: refetchUserQuery, loading } = useQuery<Query>(CurrentUserQuery, {
onCompleted: ({ currentUser }) => {
setUser(currentUser)
}
})
const refetch = async () => {
const { data: { currentUser } } = await refetchUserQuery()
setUser(currentUser)
}
if (loading) return <Loading />
if (!user) return <UnauthorizedAccess />
return (
<Context.Provider value={{ user, refetch, isOnlyTeacher, authToken }}>
{children}
</Context.Provider>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./ApolloContext";
export * from "./UserContext";

View File

@@ -1,9 +0,0 @@
import { Application } from "@hotwired/stimulus"
const application = Application.start()
// Configure Stimulus development experience
application.debug = false
window.Stimulus = application
export { application }

View File

@@ -1,7 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "Hello World!"
}
}

View File

@@ -1,11 +0,0 @@
// Import and register all your controllers from the importmap under controllers/*
import { application } from "controllers/application"
// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
// lazyLoadControllersFrom("controllers", application)

View 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>
)

View 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>
)
}

View 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>
)

View File

@@ -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}/>
)
}

View 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}/>
)
}

View File

@@ -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}/>
)
}

View 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}/>
)
}

View File

@@ -0,0 +1,4 @@
export * from "./QuestionsBySubject";
export * from "./QuestionsByBloomTaxonomy";
export * from "./QuestionsByDifficulty";
export * from "./QuestionsByCheckType";

View 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>
)
}

View File

@@ -0,0 +1 @@
export * from "./Pie";

View File

@@ -0,0 +1 @@
export * from "./charts";

View File

@@ -0,0 +1 @@
export * from "./Dashboard";

View 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>
</>
)
}

View File

@@ -0,0 +1 @@
export * from "./Edit";

View 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>
)
}

View 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>
)
}

View 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>
);
};

View 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())}
/>
)}
/>
);
};

View File

@@ -0,0 +1 @@
export * from "./Form";

View 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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -0,0 +1 @@
export * from "./FeaturesFormStep";

View File

@@ -0,0 +1,4 @@
export * from "./EnunciationFormStep";
export * from "./AnswerFormStep";
export * from "./DistractoresFormStep";
export * from "./FeaturesFromStep";

View 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>
);
};

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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 >
);
};

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,2 @@
export * from "./QuestionsFilter";
export * from "./QuestionsFilterProvider";

View 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>
)
}

View 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ê" />
</>
)
}

View 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
}}
/>
)
};

View 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
}}
/>
)
};

View 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 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
}}
/>
)
};

View File

@@ -0,0 +1 @@
export { List } from "./List";

View 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>
</>
)
};

View File

@@ -0,0 +1 @@
export * from "./New";

View 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>
</>
);
};

View File

@@ -0,0 +1 @@
export { Review } from "./Review";

View 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>
</>
);
};

View File

@@ -0,0 +1 @@
export { Show } from "./Show";

View 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);

View File

@@ -0,0 +1,5 @@
export * from "./New";
export * from "./Show";
export * from "./Review";
export * from "./Edit";
export * from "./List";

View File

@@ -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>
)
};

View File

@@ -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>
</>
)
};

View File

@@ -0,0 +1 @@
export * from './ReviewMessages'

Some files were not shown because too many files have changed in this diff Show More