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