├── .babelrc
├── .dockerignore
├── .eslintrc.json
├── .gcloudignore
├── .github
└── workflows
│ └── cypress.yaml
├── .gitignore
├── .prettierrc
├── .vscode
├── settings.json
└── tasks.json
├── Dockerfile
├── LICENSE
├── README.md
├── app.yaml
├── build
├── favicon
│ └── favicon.png
└── index.html
├── cloudbuild.yaml
├── cypress.json
├── cypress.local.json
├── cypress
├── integration
│ ├── login.spec.js
│ ├── main.spec.js
│ ├── password-reset.spec.js
│ └── register.spec.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── deploy.sh
├── docker-compose.yaml
├── favicon.gif
├── html-webpack-plugin.js
├── index.html
├── lib
└── index.js
├── package-lock.json
├── package.json
├── src
├── API
│ ├── APIError.ts
│ └── index.ts
├── HOCs
│ └── WithSubjectsData.tsx
├── __mocks__
│ ├── AllProfessors.ts
│ ├── AllSubjects.ts
│ └── MockReviews.ts
├── actions
│ └── index.ts
├── components
│ ├── Breadcrumb
│ │ └── index.tsx
│ ├── Circle
│ │ └── index.tsx
│ ├── CollapsibleText
│ │ └── index.tsx
│ ├── CompressedTextWithTooltip
│ │ └── index.tsx
│ ├── ErrorDialog
│ │ └── index.tsx
│ ├── ErrorScreen
│ │ └── index.tsx
│ ├── Footer
│ │ ├── index.tsx
│ │ └── style.css
│ ├── GeneralSearch
│ │ ├── SearchSelector
│ │ │ └── index.tsx
│ │ ├── index.tsx
│ │ └── style.css
│ ├── ImageBlock
│ │ └── index.tsx
│ ├── InfoModal
│ │ └── index.tsx
│ ├── Landing
│ │ ├── Accordion
│ │ │ └── index.tsx
│ │ ├── ContributeButton
│ │ │ ├── index.tsx
│ │ │ └── style.css
│ │ ├── Stats
│ │ │ └── index.tsx
│ │ ├── index.tsx
│ │ └── style.css
│ ├── LoadingEllipsis
│ │ └── index.tsx
│ ├── MessagePanel
│ │ └── index.tsx
│ ├── MetaInfo
│ │ └── index.tsx
│ ├── Navbar
│ │ ├── NavbarGuest.tsx
│ │ ├── NavbarUser.tsx
│ │ ├── UserMenu.tsx
│ │ ├── index.tsx
│ │ └── style.css
│ ├── PartialInput
│ │ └── index.tsx
│ ├── PasswordInput
│ │ └── index.tsx
│ ├── PasswordRedefinitionModal
│ │ └── index.tsx
│ ├── RequirementsGraph
│ │ └── index.tsx
│ ├── SendActivationEmailModal
│ │ └── index.tsx
│ ├── SimpleConfirmationDialog
│ │ └── index.tsx
│ ├── SubjectSiblings
│ │ └── index.tsx
│ ├── UpdateTranscriptModal
│ │ └── index.tsx
│ ├── VoteButton
│ │ └── index.tsx
│ ├── offerings
│ │ ├── OfferingApprovalDonut
│ │ │ └── index.tsx
│ │ ├── OfferingEmotesSelector
│ │ │ └── index.tsx
│ │ ├── OfferingReviewBalloon
│ │ │ └── index.tsx
│ │ ├── OfferingReviewBox
│ │ │ └── index.tsx
│ │ ├── OfferingReviewInput
│ │ │ ├── index.tsx
│ │ │ └── style.css
│ │ ├── OfferingReviewModal
│ │ │ └── index.tsx
│ │ ├── OfferingReviewReportDialog
│ │ │ └── index.tsx
│ │ ├── OfferingReviewsFeed
│ │ │ └── index.tsx
│ │ ├── OfferingReviewsPanel
│ │ │ └── index.tsx
│ │ └── OfferingsList
│ │ │ └── index.tsx
│ └── profile
│ │ └── VoteButton.tsx
├── contexts
│ ├── OfferingContext.ts
│ └── ReviewContext.ts
├── global.css
├── hooks
│ └── index.ts
├── images
│ ├── GithubLogo.svg
│ ├── aboutpage1.svg
│ ├── aboutpage2.svg
│ ├── aboutpage3.svg
│ ├── aboutpage4.svg
│ ├── arrow-down.svg
│ ├── authenticityCode.png
│ ├── bug_illustration.svg
│ ├── checkboxes.png
│ ├── comment_stat_icon.svg
│ ├── contribute.svg
│ ├── error-illustration.svg
│ ├── explore.svg
│ ├── grade_stat_icon.svg
│ ├── hated.svg
│ ├── hated2.svg
│ ├── indifferent.svg
│ ├── indifferent2.png
│ ├── liked.svg
│ ├── liked2.png
│ ├── logo.svg
│ ├── loved.svg
│ ├── loved2.svg
│ ├── navbar_logo.svg
│ ├── offering_stat_icon.svg
│ ├── requirements_graph_captions.svg
│ ├── reviews.svg
│ ├── search-teacher.svg
│ ├── stats.svg
│ ├── subject_review.gif
│ ├── subject_stat_icon.svg
│ ├── track_progress.gif
│ ├── unliked.svg
│ ├── unliked2.png
│ ├── user_stat_icon.svg
│ └── write-comment.svg
├── index.tsx
├── pages
│ ├── AboutPage
│ │ ├── index.tsx
│ │ └── style.css
│ ├── AccountActivationPage
│ │ └── index.tsx
│ ├── HomePage
│ │ ├── index.tsx
│ │ └── style.css
│ ├── LoginPage
│ │ ├── index.tsx
│ │ └── style.css
│ ├── NotFoundPage
│ │ ├── detective.svg
│ │ └── index.tsx
│ ├── OfferingsPage
│ │ ├── Desktop.tsx
│ │ ├── Mobile.tsx
│ │ ├── MobileOfferingSelector.tsx
│ │ └── index.tsx
│ ├── PasswordResetPage
│ │ └── index.tsx
│ ├── ProfilePage
│ │ ├── TranscriptList.tsx
│ │ ├── TranscriptTable.tsx
│ │ ├── TranscriptView.tsx
│ │ └── index.tsx
│ ├── RegisterPage
│ │ ├── index.tsx
│ │ └── style.css
│ ├── SettingsPage
│ │ └── index.tsx
│ ├── SubjectPage
│ │ ├── CreditsIndicator.tsx
│ │ ├── GradeDistributionChart.tsx
│ │ ├── index.tsx
│ │ └── style.css
│ ├── SubjectsPage
│ │ └── index.tsx
│ ├── TeachersPage
│ │ └── index.tsx
│ └── UseTermsPage
│ │ └── index.tsx
├── reducer
│ └── index.tsx
├── routes
│ ├── LoggedInRoute.tsx
│ ├── LoggedOutRoute.tsx
│ └── WithMetaRoute.tsx
├── theme
│ └── index.ts
├── types
│ ├── Auth.ts
│ ├── Course.ts
│ ├── Offering.ts
│ ├── Record.ts
│ ├── Stats.ts
│ ├── Subject.ts
│ ├── User.ts
│ ├── custom.d.ts
│ └── redux.ts
└── utils
│ ├── index.ts
│ └── time.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/env",
4 | "@babel/preset-react",
5 | "@babel/preset-typescript"
6 | ],
7 | "plugins": [
8 | "@babel/proposal-class-properties",
9 | "@babel/proposal-object-rest-spread",
10 | ["@babel/plugin-transform-runtime", {"regenerator": true}]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | .dockerignore
4 | Dockerfile
5 | Dockerfile.prod
6 | docker-compose.yaml
7 | app.yaml
8 | deploy.sh
9 | .gcloudignore
10 | cypress*
11 | .github
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2015": true
5 | },
6 | "ignorePatterns": [
7 | "node_modules/",
8 | "*.js"
9 | ],
10 | "extends": [
11 | "eslint:recommended",
12 | "plugin:react/recommended",
13 | "plugin:cypress/recommended",
14 | "plugin:@typescript-eslint/recommended",
15 | "prettier",
16 | "prettier/@typescript-eslint"
17 | ],
18 | "parser": "@typescript-eslint/parser",
19 | "parserOptions": {
20 | "ecmaFeatures": {
21 | "jsx": true
22 | },
23 | "ecmaVersion": 6,
24 | "sourceType": "module"
25 | },
26 | "plugins": [
27 | "react",
28 | "@typescript-eslint",
29 | "import-helpers",
30 | "cypress",
31 | "prettier",
32 | "unused-imports"
33 | ],
34 | "rules": {
35 | "react/prop-types": 0,
36 | "prettier/prettier": "warn",
37 | "unused-imports/no-unused-imports": "warn"
38 | },
39 | "settings": {
40 | "react": {
41 | "version": "detect"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gcloudignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib/
3 | src/
4 | .git/
5 | .gitignore
6 | .prettierc
7 | package.json
8 | tsconfig.json
9 | webpack.config.js
10 | yarn.lock
11 | yarn-error.log
12 | .prettierrc
13 | cloudbuild.yaml
14 | .eslintrc.json
15 | .env.dev
16 | .env.prod
17 | .babelrc
--------------------------------------------------------------------------------
/.github/workflows/cypress.yaml:
--------------------------------------------------------------------------------
1 | name: Cypress Tests
2 |
3 | on:
4 | push:
5 | branches: [master,dev]
6 | pull_request:
7 | branches: [master,dev]
8 |
9 | jobs:
10 | cypress-run:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 | - name: Cypress run
16 | uses: cypress-io/github-action@v2
17 | with:
18 | start: npx webpack serve --mode=development
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/static/
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # Don't track cypress videos, but keep them for uploading them on an issue, for example
107 | cypress/videos/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "jsxBracketSameLine": true,
6 | "useTabs": true,
7 | "tabWidth": 4,
8 | "bracketSameLine": false,
9 | "arrowParens": "avoid",
10 | "bracketSpacing": true
11 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.format.enable": true,
3 | "eslint.validate": [
4 | "javascript",
5 | "javascriptreact",
6 | "typescript",
7 | "typescriptreact"
8 | ],
9 | "eslint.lintTask.enable": true,
10 | "editor.codeActionsOnSave": {
11 | "source.fixAll.eslint": true
12 | },
13 | "editor.defaultFormatter": "dbaeumer.vscode-eslint"
14 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Typescript Lint",
8 | "command": "./node_modules/.bin/tsc",
9 | "args": [
10 | "-p",
11 | ".",
12 | "--noEmit"
13 | ],
14 | "isBackground": false,
15 | "problemMatcher": "$tsc",
16 | "group": {
17 | "kind": "build",
18 | "isDefault": true
19 | },
20 | "presentation": {
21 | "revealProblems": "always",
22 | }
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Built following https://mherman.org/blog/dockerizing-a-react-app/
2 |
3 | FROM node:16
4 |
5 | WORKDIR /app
6 |
7 | # so that package search will include node_modules
8 | ENV PATH /app/node_modules/.bin:$PATH
9 |
10 | # install dependencies
11 | COPY package.json ./
12 | COPY yarn.lock ./
13 | RUN yarn
14 |
15 | # copy source
16 | COPY . ./
17 |
18 | # Run webpack
19 | # --host=0.0.0.0 is necessary to access it externaly
20 | CMD ["yarn", "start", "--port=80", "--host=0.0.0.0"]
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: nodejs12 # or another supported version
2 |
3 | instance_class: F1
4 |
5 | handlers:
6 | - url: /static
7 | static_dir: build/static
8 |
9 | - url: /favicon
10 | static_dir: build/favicon
11 |
12 | - url: /.*
13 | static_files: build/index.html
14 | upload: build/index.html
15 | redirect_http_response_code: 301
16 | secure: always
--------------------------------------------------------------------------------
/build/favicon/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/build/favicon/favicon.png
--------------------------------------------------------------------------------
/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | USPY
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/cloudbuild.yaml:
--------------------------------------------------------------------------------
1 | steps:
2 | - name: node:$_NODE_VERSION
3 | entrypoint: yarn
4 | args: ['install']
5 |
6 | - name: node:$_NODE_VERSION
7 | entrypoint: yarn
8 | args: ['webpack', '--mode=$_MODE']
9 |
10 | - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
11 | entrypoint: 'bash'
12 | args: ['-c', 'gcloud config set app/cloud_build_timeout 1600 && gcloud app deploy']
13 | timeout: '1600s'
14 |
15 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://127.0.0.1:8080",
3 | "env": {
4 | "API_URL": "https://dev.uspy.me"
5 | }
6 | }
--------------------------------------------------------------------------------
/cypress.local.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://127.0.0.1",
3 | "env": {
4 | "API_URL": "http://127.0.0.1:8080"
5 | }
6 | }
--------------------------------------------------------------------------------
/cypress/integration/login.spec.js:
--------------------------------------------------------------------------------
1 | const loginUrl = Cypress.env('API_URL') + '/account/login'
2 | const sendActivationEmailUrl = Cypress.env('API_URL') + '/account/email/verification'
3 | const sendPasswordRedefinitionEmailUrl = Cypress.env('API_URL') + '/account/email/password_reset'
4 |
5 | function submitForm (nusp, pwd) {
6 | cy.get("input[name='NUSP']").type(nusp)
7 | cy.get("input[name='senha']").type(pwd)
8 |
9 | cy.contains('Entrar').click()
10 | }
11 |
12 | function dismissDialog () {
13 | cy.get('#dismiss-error-dialog').click()
14 | }
15 |
16 | function checkErrorAndDismiss (errMessage) {
17 | cy.contains(errMessage)
18 | dismissDialog()
19 | }
20 |
21 | describe('Login Page', () => {
22 | it('Should return 401 for wrong user or pwd', () => {
23 | cy.intercept('POST', loginUrl).as('loginRequest')
24 |
25 | cy.visit('/Login')
26 | cy.get("input[name='NUSP']").type('123456789')
27 | cy.get("input[name='senha']").type('Abacaba@123')
28 |
29 | cy.contains('Entrar').click()
30 | cy.wait('@loginRequest').then((interception) => {
31 | expect(interception.response.statusCode).to.equal(401)
32 | })
33 | checkErrorAndDismiss('Número USP ou senha incorretos')
34 | })
35 |
36 | it('Should return 400 if NUSP is not numeric', () => {
37 | cy.intercept('POST', loginUrl).as('loginRequest')
38 |
39 | cy.visit('/Login')
40 | submitForm('abacaba', 'Abacaba@123')
41 |
42 | cy.wait('@loginRequest').then((interception) => {
43 | expect(interception.response.statusCode).to.equal(400)
44 | })
45 | checkErrorAndDismiss('Número USP ou senha incorretos')
46 | })
47 |
48 | it('Should return 400 if password doesnt attend requirements', () => {
49 | cy.intercept('POST', loginUrl).as('loginRequest')
50 | cy.visit('/Login')
51 | submitForm('123456789', 'abacaba')
52 |
53 | cy.wait('@loginRequest').then((interception) => {
54 | expect(interception.response.statusCode).to.equal(400)
55 | })
56 | checkErrorAndDismiss('Número USP ou senha incorretos')
57 | })
58 |
59 | it('Should work properly if login request returns 200', () => {
60 | const fakeName = 'Foo Bar'
61 | cy.intercept('POST', loginUrl, { statusCode: 200, body: { user: '123456789', name: fakeName } }).as('loginRequest')
62 |
63 | cy.visit('/Login')
64 | submitForm('123456789', 'Abacaba@123')
65 |
66 | cy.wait('@loginRequest')
67 | cy.url().should('eq', Cypress.config().baseUrl + '/') // check for redirection to home page
68 | cy.contains(`Bem vindo, ${fakeName}!`) // check for snack bar
69 | })
70 |
71 | /* Disable this for now, until uspy-backend-#51 is done
72 | it('Should enable resending activation email if request returns unverified account error', () => {
73 | cy.intercept('POST', loginUrl, { statusCode: 403, body: 'e-mail ainda nao foi verificado' }).as('loginRequest')
74 | cy.intercept('POST', sendActivationEmailUrl, { statusCode: 200 }).as('sendActivationEmailRequest')
75 |
76 | cy.visit('/Login')
77 | cy.contains('Reenviar email de ativação').should('not.exist')
78 | submitForm('123456789', 'Abacaba@123')
79 |
80 | cy.on('window:alert', str => { expect(str).to.contain('Reenviar email de ativação') })
81 | cy.wait('@loginRequest')
82 |
83 | cy.contains('Reenviar email de ativação').click()
84 | cy.contains('Enviar').should('be.disabled')
85 | cy.get('#email').type('fake.email@usp.br')
86 | cy.contains('Enviar').click()
87 | cy.wait('@sendActivationEmailRequest')
88 | cy.contains('Email enviado com sucesso') // check for snack bar
89 | })
90 | */
91 |
92 | it('Should enable resetting password', () => {
93 | cy.intercept('POST', sendPasswordRedefinitionEmailUrl, { statusCode: 200 }).as('sendPasswordRedefinitionEmail')
94 |
95 | cy.visit('/Login')
96 |
97 | cy.contains('Esqueci a senha').click()
98 | cy.contains('Enviar').should('be.disabled')
99 | cy.get('#email').type('fake.email@usp.br')
100 | cy.contains('Enviar').click()
101 | cy.wait('@sendPasswordRedefinitionEmail')
102 | cy.contains('Email enviado com sucesso') // check for snack bar
103 | })
104 | })
105 |
--------------------------------------------------------------------------------
/cypress/integration/main.spec.js:
--------------------------------------------------------------------------------
1 | // main.spec.js created with Cypress
2 | //
3 | // Start writing your Cypress tests below!
4 | // If you're unfamiliar with how Cypress works,
5 | // check out the link below and learn how to write your first test:
6 | // https://on.cypress.io/writing-first-test
7 |
8 | import { buildURI as buildAboutPageURI } from 'pages/AboutPage'
9 | import { buildURI as buildLoginPageURI } from 'pages/LoginPage'
10 | import { buildURI as buildRegisterPageURI } from 'pages/RegisterPage'
11 | import { buildURI as buildSubjectsPageURI } from 'pages/SubjectsPage'
12 | import { buildURI as buildUseTermsPageURI } from 'pages/UseTermsPage'
13 |
14 | const apiURL = Cypress.env('API_URL')
15 |
16 | describe('Home Page', () => {
17 | beforeEach(() => {
18 | cy.intercept(apiURL + '/api/subject/all*').as('getAllSubjects')
19 | cy.visit('/')
20 | cy.wait('@getAllSubjects')
21 | })
22 |
23 | it('should be navigateable', () => {
24 | // Page Termos de Uso
25 | cy.contains('Termos de Uso').click()
26 | cy.url().should('match', new RegExp(buildUseTermsPageURI()))
27 | cy.go('back')
28 |
29 | // Page Sobre
30 | cy.contains('Sobre').click()
31 | cy.url().should('match', new RegExp(buildAboutPageURI()))
32 | cy.go('back')
33 |
34 | // Page Ver Lista de Disciplinas
35 | cy.contains('Ver lista de disciplinas').click()
36 | cy.url().should('match', new RegExp(buildSubjectsPageURI()))
37 | cy.go('back')
38 |
39 | // Page Login
40 | cy.contains('Login').click()
41 | cy.url().should('match', new RegExp(buildLoginPageURI()))
42 | cy.go('back')
43 |
44 | // Page cadastro
45 | cy.contains('Cadastrar').click()
46 | cy.url().should('match', new RegExp(buildRegisterPageURI()))
47 | cy.go('back')
48 |
49 | // Home page button
50 | cy.get('.toolbar img').first().click()
51 | })
52 |
53 | it('search engine should work', () => {
54 | // todo: cy.get('input').should('be.focused')
55 | cy.get('input').type('SCC0221')
56 | cy.contains('Introdução à Ciência de Computação')
57 | cy.get('button[title=Limpar]').click()
58 | cy.get('input')
59 | .should('have.value', '')
60 | .type('Intro')
61 |
62 | cy.contains('Introdução')
63 |
64 | // test that completion box disappears when clicking outside
65 | cy.get('body').click()
66 | cy.contains('Introdução').should('not.exist')
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/cypress/integration/password-reset.spec.js:
--------------------------------------------------------------------------------
1 | import { buildURI as buildLoginPageURI } from 'pages/LoginPage'
2 | import { buildURI } from 'pages/PasswordResetPage'
3 | const passwordResetUrl = Cypress.env('API_URL') + '/account/password_reset'
4 |
5 | function isInvalid (el) {
6 | return el.invoke('attr', 'aria-invalid').should('eq', 'true')
7 | }
8 | function isValid (el) {
9 | return el.invoke('attr', 'aria-invalid').should('eq', 'false')
10 | }
11 |
12 | function sendForm () {
13 | cy.get('#pwd1').clear().type('abc@1234')
14 | cy.get('#pwd2').clear().type('abc@1234')
15 | cy.get('#submit').click()
16 | }
17 |
18 | function dismissDialog () {
19 | cy.get('#dismiss-error-dialog').click()
20 | }
21 |
22 | function checkErrorAndDismiss (errMessage) {
23 | cy.contains(errMessage)
24 | dismissDialog()
25 | }
26 |
27 | describe('Password Reset Page', () => {
28 | beforeEach(() => {
29 | cy.visit(buildURI())
30 | })
31 | describe('Form should work correctly', () => {
32 | it('Should be invalid for pwd not fulfilling requirements', () => {
33 | // everything bad
34 | isInvalid(cy.get('#pwd1').type('abc').blur())
35 | cy.contains('Senha não satisfaz os requisitos') // check for helper text appearing
36 |
37 | // too short
38 | isInvalid(cy.get('#pwd1').clear().type('abc@123').blur())
39 | cy.contains('Senha não satisfaz os requisitos') // check for helper text appearing
40 |
41 | // special character missing
42 | isInvalid(cy.get('#pwd1').clear().type('abcd1234').blur())
43 | cy.contains('Senha não satisfaz os requisitos') // check for helper text appearing
44 |
45 | // number missing
46 | isInvalid(cy.get('#pwd1').clear().type('abc@ABCD').blur())
47 | cy.contains('Senha não satisfaz os requisitos') // check for helper text appearing
48 | })
49 |
50 | it('Should be valid for pwd fullfilling requirements', () => {
51 | isValid(cy.get('#pwd1').clear().type('abc@1234').blur())
52 | isValid(cy.get('#pwd2').clear().type('abc@1234').blur())
53 | cy.contains('Senha não satisfaz os requisitos').should('not.exist')
54 | cy.contains('Senhas diferem').should('not.exist')
55 | })
56 |
57 | it('Should give error for unmatching passwords', () => {
58 | cy.get('#pwd1').clear().type('abc@1234')
59 | isInvalid(cy.get('#pwd2').clear().type('abc@1235').blur())
60 | cy.contains('Senhas diferem') // check for helper text appearing
61 | })
62 |
63 | it('Button should be disabled when form is invalid', () => {
64 | cy.get('#submit').should('be.disabled')
65 |
66 | cy.get('#pwd1').type('abc').blur()
67 | cy.get('#submit').should('be.disabled')
68 |
69 | cy.get('#pwd1').clear().type('Batata@123')
70 | cy.get('#pwd2').clear().type('Batata@124')
71 | cy.get('#submit').should('be.disabled')
72 | })
73 |
74 | it('Button should be enabled when form is valid', () => {
75 | cy.get('#pwd1').clear().type('abc@1234').blur()
76 | cy.get('#pwd2').clear().type('abc@1234').blur()
77 | cy.get('#submit').should('be.enabled')
78 | })
79 | })
80 |
81 | describe('Post-form submit events', () => {
82 | it('Should give alert on invalid or missing token', () => {
83 | sendForm()
84 | checkErrorAndDismiss('Token inválido!')
85 | })
86 | it('Should give alert on missing user', () => {
87 | cy.intercept('PUT', passwordResetUrl, { statusCode: 404 })
88 | sendForm()
89 | checkErrorAndDismiss('O usuário não existe!')
90 | })
91 |
92 | it('Should give alert on unexpected error', () => {
93 | cy.intercept('PUT', passwordResetUrl, { statusCode: 500 })
94 | sendForm()
95 | checkErrorAndDismiss('Algo deu errado')
96 | })
97 |
98 | it('Should redirect to login page on success', () => {
99 | cy.intercept('PUT', passwordResetUrl, { statusCode: 200 })
100 | sendForm()
101 | cy.contains('Senha redefinida com sucesso!')
102 | cy.url().should('match', new RegExp(buildLoginPageURI()))
103 | })
104 | })
105 | })
106 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 |
20 | const webpackPreprocessor = require('@cypress/webpack-preprocessor')
21 | const path = require('path')
22 |
23 | const webpackConfig = require('../../webpack.config')({ local: true }, { mode: 'development' })
24 |
25 | module.exports = (on) => {
26 | const options = webpackPreprocessor.defaultOptions
27 |
28 | // fix bug with strict imports that require extension
29 | webpackConfig.module.rules.push({
30 | test: /\.m?js/,
31 | resolve: {
32 | fullySpecified: false
33 | }
34 | })
35 |
36 | // change root path
37 | webpackConfig.resolve = {
38 | extensions: ['.tsx', '.ts', '.js'],
39 | modules: [path.join('../..', 'node_modules'), path.join('../..', 'src')]
40 | }
41 |
42 | // fix incompatiblity with webpack 5 https://github.com/cypress-io/cypress/issues/8900
43 | let outputOptions = {}
44 | Object.defineProperty(webpackConfig, 'output', {
45 | get: () => {
46 | return { ...outputOptions, publicPath: '' }
47 | },
48 | set: function (x) {
49 | outputOptions = x
50 | }
51 | })
52 |
53 | options.webpackOptions = webpackConfig
54 |
55 | on('file:preprocessor', webpackPreprocessor(options))
56 | }
57 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | GREEN='\e[1;32m'
4 | NC='\e[0m' # No Color
5 |
6 | MASTER_BRANCH=master
7 | DEV_BRANCH=dev
8 | CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD)
9 |
10 | get_major_minor_patch() {
11 | regex="v(.*)\\.(.*)\\.(.*)"
12 | if [[ $1 =~ $regex ]]; then
13 | echo "Currently in ${BASH_REMATCH[0]}"
14 | MAJOR=${BASH_REMATCH[1]}
15 | MINOR=${BASH_REMATCH[2]}
16 | PATCH=${BASH_REMATCH[3]}
17 | else
18 | echo "DIDNT MATCH"
19 | fi
20 | }
21 |
22 | if [ $# = 0 ] || [ "$1" = "help" ] || [ "$1" = "--help" ]; then
23 | echo "Usage: ./deploy.sh \"\" [tag]"
24 | exit 0
25 | fi
26 |
27 | # check if switching to master will break.
28 | # if so, do it and print the error
29 | git checkout $MASTER_BRANCH &> /dev/null
30 | if [ "$?" != "0" ]; then
31 | git checkout $MASTER_BRANCH || exit 1
32 | fi
33 |
34 | MESSAGE="$1"
35 |
36 |
37 | git pull --rebase
38 | if [ "$?" != "0" ]; then
39 | git checkout $MASTER_BRANCH || exit 1
40 | fi
41 | # get the tag
42 | TAG=$(git describe --tags --abbrev=0)
43 |
44 | # go back to previous branch (remember we switched to master)
45 | git checkout $CUR_BRANCH &> /dev/null
46 |
47 | if [ $# -lt 2 ]; then
48 |
49 | # parse the current tag
50 | get_major_minor_patch $TAG || exit 1
51 |
52 | while [[ 0 ]]
53 | do
54 | read -p "Please tell the type of release (patch/minor/major): " type
55 |
56 | if [ $type = "patch" ]; then
57 | let PATCH++
58 | elif [ $type = "minor" ]; then
59 | let MINOR++
60 | PATCH=0
61 | elif [ $type = "major" ]; then
62 | let MAJOR++
63 | MINOR=0
64 | PATCH=0
65 | else
66 | echo "Choose one of the options"
67 | continue
68 | fi
69 | break
70 | done
71 |
72 | NEW_TAG=v$MAJOR.$MINOR.$PATCH
73 |
74 | else
75 | NEW_TAG=v$2
76 | fi
77 |
78 | prompt_and_continue () {
79 | echo -e $1
80 | read -p "Continue? (Y/n) " aux
81 | }
82 |
83 | prompt_and_continue "Releasing ${GREEN}${NEW_TAG}${NC}."
84 |
85 | if [ "$aux" = "n" ] || [ "$aux" = "N" ]; then
86 | exit 0
87 | fi
88 |
89 | # go to master and merge
90 | git checkout $MASTER_BRANCH || exit 1
91 | git merge --no-ff $CUR_BRANCH -m "$MESSAGE" || exit 1
92 |
93 | # create tag
94 | git tag -a $NEW_TAG -m "$MESSAGE" || exit 1
95 |
96 | echo
97 |
98 | # push branch
99 | git push --follow-tags || exit 1
100 |
101 | # return to original branch
102 | git checkout $CUR_BRANCH
103 |
104 | echo
105 |
106 | echo -e "${GREEN}Version released successfully!${NC}"
107 | echo
108 | echo "Summary:"
109 | echo "- Merged $CUR_BRANCH to $MASTER_BRANCH"
110 | echo "- Released version $NEW_TAG with message $MESSAGE"
111 | echo "- Push to master"
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.2"
2 | services:
3 | web:
4 | container_name: uspy-frontend
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | volumes:
9 | - '.:/app'
10 | ports:
11 | - 80:80
12 | environment:
13 | - CHOKIDAR_USEPOLLING=true
--------------------------------------------------------------------------------
/favicon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/favicon.gif
--------------------------------------------------------------------------------
/html-webpack-plugin.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin')
2 |
3 | class MyPlugin {
4 | apply (compiler) {
5 | compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
6 | // Static Plugin interface |compilation |HOOK NAME | register listener
7 | HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
8 | 'MyPlugin', // <-- Set a meaningful name here for stacktraces
9 | (data, cb) => {
10 | // Manipulate the content
11 | // Tell webpack to move on
12 | cb(null, data)
13 | }
14 | )
15 | })
16 | }
17 | }
18 |
19 | module.exports = MyPlugin
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | É preciso habilitar o JavaScript para poder usar este site!
12 |
13 |
14 | USPY
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _react = _interopRequireDefault(require("react"));
4 |
5 | var _reactDom = _interopRequireDefault(require("react-dom"));
6 |
7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
8 |
9 | var App = function App() {
10 | var x = "1";
11 | var y;
12 | y = x;
13 | return /*#__PURE__*/_react["default"].createElement("h1", null, " Hello World ", y, " ");
14 | };
15 |
16 | _reactDom["default"].render( /*#__PURE__*/_react["default"].createElement(App, null), document.getElementById('root'));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uspy-frontend",
3 | "version": "0.0.1",
4 | "description": "Front-end of uspy project",
5 | "main": "index.tsx",
6 | "repository": "https://github.com/Projeto-USPY/uspy-frontend",
7 | "author": "Lucas Turci and Thiago Preischadt",
8 | "scripts": {
9 | "start": "webpack serve --mode=development --env local",
10 | "local": "webpack serve --mode=development --env local --port 3000",
11 | "cypress": "cypress run -C cypress.local.json",
12 | "cypress-ui": "cypress open -C cypress.local.json"
13 | },
14 | "license": "MIT",
15 | "private": true,
16 | "dependencies": {
17 | "@material-ui/core": "^4.11.0",
18 | "@material-ui/icons": "^4.9.1",
19 | "@material-ui/lab": "^4.0.0-alpha.57",
20 | "add": "^2.0.6",
21 | "axios": "^0.21.0",
22 | "axios-retry": "^3.1.9",
23 | "match-sorter": "^6.3.1",
24 | "notistack": "^1.0.5",
25 | "react": "^16.13.1",
26 | "react-archer": "^3.0.0",
27 | "react-dom": "^16.13.1",
28 | "react-helmet": "^6.1.0",
29 | "react-redux": "^8.1.1",
30 | "react-router": "^5.2.0",
31 | "react-router-dom": "^5.2.0",
32 | "recharts": "^2.0.3",
33 | "redux": "^4.0.5",
34 | "universal-cookie": "^4.0.4"
35 | },
36 | "devDependencies": {
37 | "@babel/cli": "^7.11.6",
38 | "@babel/core": "^7.11.6",
39 | "@babel/plugin-proposal-class-properties": "^7.10.4",
40 | "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
41 | "@babel/plugin-transform-runtime": "^7.12.10",
42 | "@babel/preset-env": "^7.11.5",
43 | "@babel/preset-react": "^7.10.4",
44 | "@babel/preset-typescript": "^7.10.4",
45 | "@babel/runtime": "^7.12.5",
46 | "@cypress/webpack-preprocessor": "^5.9.1",
47 | "@svgr/webpack": "^8.0.0",
48 | "@types/material-ui": "^0.21.8",
49 | "@types/react": "^18.0.28",
50 | "@types/react-dom": "^18.0.28",
51 | "@types/react-helmet": "^6.1.5",
52 | "@types/react-redux": "^7.1.12",
53 | "@types/react-router-dom": "^5.1.6",
54 | "@types/recharts": "^1.8.19",
55 | "@typescript-eslint/eslint-plugin": "^5.61.0",
56 | "@typescript-eslint/parser": "^5.61.0",
57 | "babel-eslint": "^10.1.0",
58 | "babel-loader": "^8.1.0",
59 | "css-loader": "^4.3.0",
60 | "cypress": "^8.2.0",
61 | "dotenv": "^8.2.0",
62 | "eslint": "^8.44.0",
63 | "eslint-config-prettier": "^6.12.0",
64 | "eslint-config-standard": "^14.1.1",
65 | "eslint-plugin-cypress": "^2.11.3",
66 | "eslint-plugin-import": "^2.22.0",
67 | "eslint-plugin-import-helpers": "^1.1.0",
68 | "eslint-plugin-node": "^11.1.0",
69 | "eslint-plugin-prettier": "^3.1.4",
70 | "eslint-plugin-promise": "^4.2.1",
71 | "eslint-plugin-react": "^7.21.2",
72 | "eslint-plugin-standard": "^4.0.1",
73 | "eslint-plugin-unused-imports": "^2.0.0",
74 | "eslint-webpack-plugin": "^4.0.1",
75 | "html-webpack-plugin": "^5.3.1",
76 | "prettier": "^2.1.2",
77 | "style-loader": "^1.2.1",
78 | "ts-loader": "^8.0.4",
79 | "typescript": "^4.0.3",
80 | "webpack": "^5.49.0",
81 | "webpack-cli": "^4.7.2",
82 | "webpack-dev-server": "^3.11.2"
83 | },
84 | "resolutions": {
85 | "@types/react": "18.0.28"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/API/APIError.ts:
--------------------------------------------------------------------------------
1 | class APIError extends Error {
2 | code: string
3 | status: number
4 |
5 | constructor(code: string, message: string, status?: number) {
6 | super(message)
7 | this.code = code
8 | this.status = status
9 | }
10 | }
11 |
12 | export const statusCodeToError: { [index: number]: string } = {
13 | 400: 'bad_request',
14 | 401: 'unauthorized',
15 | 403: 'forbidden',
16 | 404: 'not_found',
17 | 405: 'method_not_allowed',
18 | 429: 'too_many_requests',
19 |
20 | 500: 'internal_server_error',
21 | 501: 'not_implemented',
22 | 502: 'bad_gateway',
23 | 503: 'service_unavailable',
24 | }
25 |
26 | export default APIError
27 |
--------------------------------------------------------------------------------
/src/HOCs/WithSubjectsData.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useEffect,
4 | useState,
5 | PropsWithChildren,
6 | } from 'react'
7 |
8 | import { CourseComplete } from 'types/Course'
9 | import { useMySnackbar } from 'hooks'
10 |
11 | import api from 'API'
12 | export const CourseDataContext: React.Context =
13 | createContext(null)
14 |
15 | interface WithSubjectsDataProps extends PropsWithChildren {
16 | course: string
17 | specialization: string
18 | }
19 |
20 | const WithSubjectsData = ({
21 | children,
22 | course,
23 | specialization,
24 | }: WithSubjectsDataProps) => {
25 | const [subjectsData, setSubjectsData] = useState(null)
26 |
27 | const notify = useMySnackbar()
28 |
29 | useEffect(() => {
30 | api.getSubjectList(course, specialization)
31 | .then(courseData => {
32 | setSubjectsData(courseData)
33 | })
34 | .catch(err => {
35 | console.error(err)
36 | notify('Erro ao carregar disciplinas', 'error')
37 | })
38 | }, [])
39 |
40 | if (subjectsData == null) {
41 | return <>{children}>
42 | } else {
43 | return (
44 |
45 | {children}
46 |
47 | )
48 | }
49 | }
50 |
51 | export default WithSubjectsData
52 |
--------------------------------------------------------------------------------
/src/__mocks__/MockReviews.ts:
--------------------------------------------------------------------------------
1 | const mockReviews = [
2 | {
3 | uuid: '1',
4 | rating: 4,
5 | body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur',
6 | edited: false,
7 | timestamp: '2021-11-07T15:50:06.506Z',
8 | upvotes: 3,
9 | downvotes: 1,
10 | },
11 | {
12 | uuid: '2',
13 | rating: 1,
14 | body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur',
15 | edited: false,
16 | timestamp: '2021-10-07T12:50:06.506Z',
17 | upvotes: 2,
18 | downvotes: 0,
19 | },
20 | {
21 | uuid: '3',
22 | rating: 5,
23 | body: 'Esse professor é horroroso! Eu odeio ele',
24 | edited: true,
25 | timestamp: '2021-09-07T12:50:06.506Z',
26 | upvotes: 0,
27 | downvotes: 10,
28 | },
29 | {
30 | uuid: '4',
31 | rating: 3,
32 | body: 'Olha aqui, as provas desse cara não são nada compatíveis com as listas que ele dá',
33 | edited: true,
34 | timestamp: '2021-10-07T12:50:06.506Z',
35 | upvotes: 4,
36 | downvotes: 0,
37 | },
38 | ]
39 |
40 | export default mockReviews
41 |
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | import { ReduxAction } from 'types/redux'
2 | import { User } from 'types/User'
3 |
4 | export const setUser = (user: User): ReduxAction => ({
5 | type: 'LOGIN',
6 | payload: user,
7 | })
8 |
9 | export const setUserNone = (): ReduxAction => ({
10 | type: 'LOGOUT',
11 | payload: null,
12 | })
13 |
14 | export const setLastUpdatedAccount = (date: string): ReduxAction => ({
15 | type: 'SET_LAST_UPDATED_ACCOUNT',
16 | payload: date,
17 | })
18 |
19 | export const uspyAlert = (message?: string, title?: string): ReduxAction => ({
20 | type: 'ALERT',
21 | payload: message
22 | ? {
23 | message,
24 | title,
25 | }
26 | : null,
27 | })
28 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import MUIBreadcrumb from '@material-ui/core/Breadcrumbs'
5 | import Typography from '@material-ui/core/Typography'
6 |
7 | interface MyLink {
8 | text: string
9 | url: string
10 | }
11 | interface Props {
12 | links: MyLink[]
13 | }
14 | const Breadcrumb: React.FC = ({ links }) => {
15 | return (
16 | '}>
17 | {links.map((link, idx) => {
18 | return (
19 |
20 | {' '}
21 |
22 | {' '}
23 | {link.text}
24 | {' '}
25 |
26 | )
27 | })}
28 |
29 | )
30 | }
31 |
32 | export default Breadcrumb
33 |
--------------------------------------------------------------------------------
/src/components/Circle/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react'
2 |
3 | import { useTheme } from '@material-ui/core/styles'
4 |
5 | interface PropsType {
6 | size: number
7 | color?: 'primary' | 'secondary'
8 | }
9 |
10 | const Circle: React.FC> = ({
11 | size,
12 | children,
13 | color = 'secondary',
14 | }) => {
15 | const theme = useTheme()
16 |
17 | const circleStyle = {
18 | height: size,
19 | width: size,
20 | display: 'flex',
21 | justifyContent: 'center',
22 | alignItems: 'center',
23 | backgroundColor: 'white',
24 | border: '1px solid blue',
25 | borderRadius: '50%',
26 | borderColor: theme.palette[color].main,
27 | color: theme.palette[color].main,
28 | }
29 |
30 | return {children}
31 | }
32 |
33 | export default Circle
34 |
--------------------------------------------------------------------------------
/src/components/CollapsibleText/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import Link from '@material-ui/core/Link'
4 |
5 | interface Props {
6 | component: React.ElementType
7 | childrenProps: any
8 | text: string
9 | maxCharacters: number
10 | }
11 |
12 | const CollapsibleText: React.FC = ({
13 | component: Child,
14 | childrenProps,
15 | text,
16 | maxCharacters,
17 | }) => {
18 | const [collapsed, setCollapsed] = useState(true)
19 |
20 | if (text.length <= maxCharacters)
21 | return {text}
22 | return (
23 |
24 | {collapsed ? text.substring(0, maxCharacters) + '...' : text}
25 | setCollapsed(!collapsed)}>
30 | {' '}
31 | {collapsed ? 'ver mais' : 'ver menos'}
32 |
33 |
34 | )
35 | }
36 |
37 | export default CollapsibleText
38 |
--------------------------------------------------------------------------------
/src/components/CompressedTextWithTooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Tooltip, { TooltipProps } from '@material-ui/core/Tooltip'
4 |
5 | interface PropsType {
6 | text: string
7 | maxCharacters: number
8 | tooltipProps?: Partial
9 | component?: React.ElementType
10 | }
11 |
12 | const CompressedTextWithTooltip: React.FC = ({
13 | text,
14 | maxCharacters,
15 | tooltipProps = null,
16 | component: Node = 'span',
17 | }) => {
18 | const isBigger = text.length > maxCharacters
19 | return (
20 |
21 |
22 | {' '}
23 | {isBigger
24 | ? text.substr(0, maxCharacters - 3) + '...'
25 | : text}{' '}
26 |
27 |
28 | )
29 | }
30 |
31 | export default CompressedTextWithTooltip
32 |
--------------------------------------------------------------------------------
/src/components/ErrorDialog/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Dialog from '@material-ui/core/Dialog'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import DialogContentText from '@material-ui/core/DialogContentText'
8 | import DialogTitle from '@material-ui/core/DialogTitle'
9 |
10 | interface PropsType {
11 | message: string
12 | open: boolean
13 | title?: string
14 | close: () => void
15 | }
16 |
17 | const ErrorDialog: React.FC = ({ message, open, title, close }) => {
18 | // next useStates and useEffect are a hack
19 | // so that message and title freeze during exit transition
20 | const [mmessage, setMessage] = useState(message)
21 | const [ttitle, setTitle] = useState(title)
22 |
23 | useEffect(() => {
24 | if (open) {
25 | setMessage(message)
26 | setTitle(title)
27 | }
28 | }, [open])
29 |
30 | return (
31 |
32 | {ttitle || 'Ops!'}
33 |
34 | {mmessage}
35 |
36 |
37 |
41 | Ok
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | export default ErrorDialog
49 |
--------------------------------------------------------------------------------
/src/components/ErrorScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Container from '@material-ui/core/Container'
4 | import Grid from '@material-ui/core/Grid'
5 | import { useTheme } from '@material-ui/core/styles'
6 | import Typography from '@material-ui/core/Typography'
7 | import useMediaQuery from '@material-ui/core/useMediaQuery'
8 |
9 | import BreadCrumb from 'components/Breadcrumb'
10 | import BugIllustration from 'images/bug_illustration.svg'
11 |
12 | interface BreadcrumbLink {
13 | text: string
14 | url: string
15 | }
16 |
17 | interface PropsType {
18 | message: string
19 | breadcrumbs?: BreadcrumbLink[]
20 | }
21 |
22 | const ErrorScreen: React.FC = ({ message, breadcrumbs }) => {
23 | const theme = useTheme()
24 | const isDesktop = useMediaQuery(theme.breakpoints.up('sm'))
25 |
26 | return (
27 |
28 |
29 |
30 | {breadcrumbs ? : null}
31 |
32 |
38 |
39 |
42 | {' '}
43 | {message}{' '}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | )
54 | }
55 |
56 | export default ErrorScreen
57 |
--------------------------------------------------------------------------------
/src/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { useMediaQuery } from '@material-ui/core'
4 | import Grid from '@material-ui/core/Grid'
5 | import Link from '@material-ui/core/Link'
6 | import { makeStyles, createStyles, useTheme } from '@material-ui/core/styles'
7 | import Typography from '@material-ui/core/Typography'
8 |
9 | import GithubLogo from 'images/GithubLogo.svg'
10 | import { buildURI as buildAboutPageURI } from 'pages/AboutPage'
11 | import { buildURI as buildUseTermsPageURI } from 'pages/UseTermsPage'
12 |
13 | const useStyles = makeStyles(theme =>
14 | createStyles({
15 | footer: {
16 | backgroundColor: theme.palette.primary.main,
17 | color: 'white',
18 | height: '100px',
19 | },
20 | }),
21 | )
22 |
23 | const githubLink = 'https://github.com/projeto-uspy'
24 | const text = 'Made with ❤ by Preischadt and Turci'
25 |
26 | const Footer: React.FC = () => {
27 | const theme = useTheme()
28 | const isDesktop = useMediaQuery(theme.breakpoints.up('sm'))
29 | const classes = useStyles()
30 | return (
31 |
36 |
37 |
38 |
41 | Termos de Uso
42 |
43 |
44 |
45 |
46 | Sobre
47 |
48 |
49 |
50 |
51 |
52 |
window.open(githubLink, '_blank')}
58 | />
59 |
60 |
61 | {isDesktop ? (
62 |
68 | {text}
69 |
70 | ) : null}
71 |
72 | )
73 | }
74 |
75 | export default Footer
76 |
--------------------------------------------------------------------------------
/src/components/Footer/style.css:
--------------------------------------------------------------------------------
1 | .move-up-hover-child {
2 | -webkit-transition: all 0.2s;
3 | transition: all 0.2s;
4 | }
5 |
6 | .move-up-hover-parent:hover .move-up-hover-child {
7 | -webkit-transform: translateY(-5%) scale(1.2, 1.2);
8 | transform: translateY(-5%) scale(1.2, 1.2);
9 | background: radial-gradient(circle, rgba(120,120,120,0.4) 0%, rgba(200,200,200,0.1) 5%, rgba(0,0,0,0) 100%);
10 | }
--------------------------------------------------------------------------------
/src/components/GeneralSearch/SearchSelector/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import TextField from '@material-ui/core/TextField'
4 | import Autocomplete from '@material-ui/lab/Autocomplete'
5 |
6 | interface PropsType {
7 | options: any[]
8 | onChange: (o: any) => void
9 | getOptionLabel: (o: any) => string
10 | value: { code: string; name: string }
11 | label: string
12 | hidden: boolean
13 | }
14 |
15 | const SearchSelector: React.FC = ({
16 | options,
17 | onChange,
18 | getOptionLabel,
19 | value,
20 | label,
21 | hidden,
22 | }) => {
23 | return (
24 | onChange(value)}
39 | renderInput={params => (
40 |
47 | )}
48 | />
49 | )
50 | }
51 |
52 | export default SearchSelector
53 |
--------------------------------------------------------------------------------
/src/components/GeneralSearch/style.css:
--------------------------------------------------------------------------------
1 | .inputfield {
2 | width: 100%;
3 | margin-top: 2rem;
4 | }
--------------------------------------------------------------------------------
/src/components/ImageBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Grid, { GridSize } from '@material-ui/core/Grid'
4 | import Typography from '@material-ui/core/Typography'
5 | import useMediaQuery from '@material-ui/core/useMediaQuery'
6 | import { useTheme } from '@material-ui/core/styles'
7 |
8 | interface ImageBlockProps {
9 | imageSource: string
10 | title: string
11 | size: 'small' | 'medium' | 'large'
12 | caption?: string
13 | center?: boolean
14 | }
15 |
16 | const ImageBlock: React.FC = ({
17 | imageSource,
18 | title,
19 | size,
20 | caption,
21 | center = false,
22 | }) => {
23 | const fullWidth = {
24 | width: '100%',
25 | }
26 |
27 | const theme = useTheme()
28 | const isMD = useMediaQuery(theme.breakpoints.up('md'))
29 |
30 | // number of cols for each size
31 | let colsXS: GridSize = 10
32 | let colsMD: GridSize = 10
33 | let colsLG: GridSize = 10
34 | if (size === 'small') {
35 | colsXS = 12
36 | colsMD = 8
37 | colsLG = 4
38 | } else if (size === 'medium') {
39 | colsXS = 12
40 | colsMD = 10
41 | colsLG = 8
42 | } else {
43 | colsXS = 12
44 | colsMD = 10
45 | colsLG = 10
46 | }
47 |
48 | const margin = {
49 | marginLeft: isMD ? '0 1rem' : '0',
50 | marginRight: isMD ? '0 1rem' : '0',
51 | marginTop: '2rem',
52 | marginBottom: '2rem',
53 | }
54 |
55 | return (
56 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {' '}
67 | {caption}
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | export default ImageBlock
76 |
--------------------------------------------------------------------------------
/src/components/InfoModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Backdrop from '@material-ui/core/Backdrop'
4 | import Box from '@material-ui/core/Box'
5 | import Button from '@material-ui/core/Button'
6 | import Divider from '@material-ui/core/Divider'
7 | import Fade from '@material-ui/core/Fade'
8 | import Grid from '@material-ui/core/Grid'
9 | import IconButton from '@material-ui/core/IconButton'
10 | import Link from '@material-ui/core/Link'
11 | import Modal from '@material-ui/core/Modal'
12 | import Typography from '@material-ui/core/Typography'
13 | import useMediaQuery from '@material-ui/core/useMediaQuery'
14 | import CloseIcon from '@material-ui/icons/CloseOutlined'
15 | import { useTheme } from '@material-ui/core/styles'
16 |
17 | import ImageBlock from 'components/ImageBlock'
18 | import AuthenticityCodeImage from 'images/authenticityCode.png'
19 | import CheckboxesImage from 'images/checkboxes.png'
20 | import { buildURI as buildUseTermsPageURI } from 'pages/UseTermsPage'
21 |
22 | interface InfoModalProps {
23 | open: boolean
24 | handleClose: () => void
25 | }
26 |
27 | const InfoModal: React.FC = ({ open, handleClose }) => {
28 | // Style stuff
29 | const theme = useTheme()
30 | const isDesktop = useMediaQuery(theme.breakpoints.up('sm'))
31 |
32 | return (
33 |
50 |
51 |
59 |
63 |
64 |
69 |
76 |
77 |
78 |
79 |
80 |
81 | {' '}
82 | Como obter o código de autenticidade?{' '}
83 |
84 |
85 | 1. Entre
86 | em{' '}
87 |
91 | {' '}
92 | JupiterWeb{' '}
93 |
94 | , escolha seu menu e curso, e marque as caixas
95 | conforme a imagem:{' '}
96 |
97 |
103 |
104 | {' '}
105 | 2. {' '}
106 | Clique em {'"Buscar"'}, e o seu histórico escolar
107 | será gerado com um código de autenticidade no topo
108 | do documento, como na imagem:{' '}
109 |
110 |
116 |
117 |
118 | 3. É
119 | este código de 16 dígitos que será usado para
120 | verificar que você está atrelado à USP e pode se
121 | cadastrar.
122 |
123 |
124 |
125 |
126 | {' '}
127 |
128 | {' '}
129 | O que fazemos com o seu histórico escolar?{' '}
130 |
131 |
132 | Seu histórico escolar é usado para extrair seus
133 | dados e informações sobre: disciplinas que cursou,
134 | médias que obteve e status de aprovação. Antes de
135 | irem para o banco de dados, eles são anonimizados e
136 | criptografados. Nós garantimos total transparência,
137 | e você pode ler mais sobre isso no nosso documento
138 | de{' '}
139 |
143 | {' '}
144 | termos e condições
145 |
146 | .{' '}
147 |
148 |
149 | {' '}
150 | O USPY funciona graças
151 | à colaboração dos usuários em oferecer seus dados
152 | para alimentar as estatísticas de aprovações, médias
153 | e reviews de disciplinas.{' '}
154 |
155 |
156 |
162 | Ok
163 |
164 |
165 |
166 |
167 |
168 | )
169 | }
170 |
171 | export default InfoModal
172 |
--------------------------------------------------------------------------------
/src/components/Landing/Accordion/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import MuiAccordion from '@material-ui/core/Accordion'
4 | import MuiAccordionDetails from '@material-ui/core/AccordionDetails'
5 | import MuiAccordionSummary from '@material-ui/core/AccordionSummary'
6 | import { withStyles } from '@material-ui/core/styles'
7 | import Typography from '@material-ui/core/Typography'
8 |
9 | const Accordion = withStyles({
10 | root: {
11 | border: '1px solid rgba(0, 0, 0, .125)',
12 | boxShadow: 'none',
13 | '&:not(:last-child)': {
14 | borderBottom: 0,
15 | },
16 | '&:before': {
17 | display: 'none',
18 | },
19 | '&$expanded': {
20 | margin: 'auto',
21 | },
22 | },
23 | expanded: {},
24 | })(MuiAccordion)
25 |
26 | const AccordionSummary = withStyles({
27 | root: {
28 | backgroundColor: 'rgba(0, 0, 0, .03)',
29 | borderBottom: '1px solid rgba(0, 0, 0, .125)',
30 | marginBottom: -1,
31 | minHeight: 56,
32 | '&$expanded': {
33 | minHeight: 56,
34 | },
35 | },
36 | content: {
37 | '&$expanded': {
38 | margin: '12px 0',
39 | },
40 | },
41 | expanded: {},
42 | })(MuiAccordionSummary)
43 |
44 | const AccordionDetails = withStyles(theme => ({
45 | root: {
46 | padding: theme.spacing(2),
47 | },
48 | }))(MuiAccordionDetails)
49 |
50 | interface AccordionCardPropsType {
51 | id: string
52 | header: string
53 | description: string
54 | expanded: boolean
55 |
56 | handleChange: (
57 | panel: string,
58 | ) => (event: React.ChangeEvent, expanded: boolean) => void
59 | }
60 |
61 | const AccordionCard: React.FC = ({
62 | id,
63 | header,
64 | description,
65 | expanded,
66 | handleChange,
67 | }) => {
68 | return (
69 |
70 |
75 |
76 | {description}
77 |
78 |
79 | )
80 | }
81 |
82 | const FAQ: React.FC = () => {
83 | const [expanded, setExpanded] = React.useState('panel1')
84 |
85 | const handleChange =
86 | (panel: string) => (_: unknown, newExpanded: boolean) => {
87 | setExpanded(newExpanded ? panel : false)
88 | }
89 |
90 | return (
91 |
92 |
101 |
109 |
119 |
127 |
135 |
136 | )
137 | }
138 |
139 | export default FAQ
140 |
--------------------------------------------------------------------------------
/src/components/Landing/ContributeButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.css'
3 |
4 | import { useTheme } from '@material-ui/core/styles'
5 |
6 | interface PropsType {
7 | text: string
8 | url: string
9 | }
10 |
11 | const ContributeButton: React.FC = ({ text, url }) => {
12 | const theme = useTheme()
13 | return (
14 |
17 | {text}
18 |
19 | )
20 | }
21 |
22 | export default ContributeButton
23 |
--------------------------------------------------------------------------------
/src/components/Landing/ContributeButton/style.css:
--------------------------------------------------------------------------------
1 | .btn {
2 | color: white;
3 | border: none;
4 | border-radius: 10px;
5 | padding: 12px;
6 | font-weight: bolder;
7 |
8 | transition: all .1s ease-in-out;
9 | cursor: pointer;
10 | }
11 |
12 | .btn:hover {
13 | transform: scale(1.03) rotate(1deg);
14 | }
15 |
16 | a {
17 | color: inherit;
18 | text-decoration: none;
19 | }
--------------------------------------------------------------------------------
/src/components/Landing/Stats/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | import Grid from '@material-ui/core/Grid'
4 |
5 | import { Stats as APIStats } from 'types/Stats'
6 |
7 | import api from 'API'
8 | import commentStatIcon from 'images/comment_stat_icon.svg'
9 | import gradeStatIcon from 'images/grade_stat_icon.svg'
10 | import offeringStatIcon from 'images/offering_stat_icon.svg'
11 | import subjectStatIcon from 'images/subject_stat_icon.svg'
12 | import userStatIcon from 'images/user_stat_icon.svg'
13 |
14 | const Stats: React.FC = () => {
15 | const [stats, setStats] = useState({} as APIStats)
16 |
17 | useEffect(() => {
18 | api.getStats().then(res => {
19 | setStats(res)
20 | })
21 | }, [])
22 |
23 | return (
24 |
29 |
30 |
35 | {stats.subjects ?? '0'} disciplinas
36 |
37 |
38 |
43 | {stats.offerings ?? '0'} oferecimentos
44 |
45 |
46 |
51 | {stats.users ?? '0'} usuários
52 |
53 |
54 |
59 | {stats.grades ?? '0'} notas
60 |
61 |
62 |
67 | {stats.comments ?? '0'} comentários
68 |
69 |
70 | )
71 | }
72 |
73 | export default Stats
74 |
--------------------------------------------------------------------------------
/src/components/Landing/style.css:
--------------------------------------------------------------------------------
1 | .link {
2 | text-decoration: none;
3 | font-weight: bold;
4 | }
--------------------------------------------------------------------------------
/src/components/LoadingEllipsis/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 |
3 | interface LoadingEllipsisProps {
4 | interval?: number
5 | }
6 | const LoadingEllipsis = ({ interval = 1000 }: LoadingEllipsisProps) => {
7 | const ref = useRef(null)
8 |
9 | useEffect(() => {
10 | const intervalID = setInterval(() => {
11 | if (ref.current == null) return
12 | if (ref.current.innerText === '...') {
13 | ref.current.innerText = ''
14 | } else {
15 | ref.current.innerText += '.'
16 | }
17 | }, interval)
18 |
19 | return () => {
20 | clearInterval(intervalID)
21 | }
22 | })
23 |
24 | return
25 | }
26 |
27 | export default LoadingEllipsis
28 |
--------------------------------------------------------------------------------
/src/components/MessagePanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Grid from '@material-ui/core/Grid'
5 | import Typography from '@material-ui/core/Typography'
6 |
7 | interface props {
8 | message: string
9 | height: number
10 | action?: () => void
11 | actionTitle?: string
12 | }
13 |
14 | const MessagePanel: React.FC = ({
15 | message,
16 | height,
17 | action,
18 | actionTitle,
19 | }) => {
20 | const style: React.CSSProperties = {
21 | height,
22 | flexGrow: 1,
23 | display: 'flex',
24 | flexDirection: 'column',
25 | justifyContent: 'center',
26 | alignItems: 'center',
27 | textAlign: 'center',
28 | border: '1px solid #BBBBBB',
29 | boxShadow: 'inset 0 0 3px 1px #BBBBBB',
30 | padding: '5px',
31 | }
32 | return (
33 |
34 |
35 | {message}
36 | {action ? (
37 | <>
38 |
39 |
43 | {' '}
44 | {actionTitle}{' '}
45 |
46 | >
47 | ) : null}
48 |
49 |
50 | )
51 | }
52 |
53 | export default memo(MessagePanel)
54 |
--------------------------------------------------------------------------------
/src/components/MetaInfo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from 'react-helmet'
3 |
4 | interface MetaInfoPropsType {
5 | title?: string
6 | description?: string
7 | robots?: string[]
8 | }
9 |
10 | const MetaInfo: React.FC = ({
11 | title,
12 | description,
13 | robots,
14 | }) => {
15 | return (
16 |
17 | {title ? {title} : null}
18 | {description ? (
19 |
20 | ) : null}
21 | {robots ? : null}
22 |
23 | )
24 | }
25 |
26 | export default MetaInfo
27 |
--------------------------------------------------------------------------------
/src/components/Navbar/NavbarGuest.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, memo } from 'react'
2 | import { useHistory } from 'react-router-dom'
3 |
4 | import { ButtonGroup } from '@material-ui/core'
5 | import Button from '@material-ui/core/Button'
6 | import Collapse from '@material-ui/core/Collapse'
7 | import IconButton from '@material-ui/core/IconButton'
8 | import Paper from '@material-ui/core/Paper'
9 | import { useTheme } from '@material-ui/core/styles'
10 | import Toolbar from '@material-ui/core/Toolbar'
11 | import useMediaQuery from '@material-ui/core/useMediaQuery'
12 | import MenuIcon from '@material-ui/icons/Menu'
13 |
14 | import Logo from 'images/navbar_logo.svg'
15 | import { buildURI as buildHomePageURI } from 'pages/HomePage'
16 | import { buildURI as buildLoginPageURI } from 'pages/LoginPage'
17 | import { buildURI as buildRegisterPageURI } from 'pages/RegisterPage'
18 |
19 | import './style.css'
20 |
21 | const buttonsGuest = [
22 | { title: 'Login', route: buildLoginPageURI() },
23 | { title: 'Cadastrar', route: buildRegisterPageURI() },
24 | ]
25 |
26 | const Navbar: React.FC = () => {
27 | const theme = useTheme()
28 | const history = useHistory()
29 | const isLarge = useMediaQuery(theme.breakpoints.up('sm'))
30 | const [isMobileMenuVisible, setIsMobileMenuVisible] = useState(false)
31 |
32 | if (isLarge && isMobileMenuVisible) setIsMobileMenuVisible(false)
33 |
34 | const goHome = () => {
35 | if (history.location.pathname !== buildHomePageURI())
36 | history.push(buildHomePageURI())
37 | }
38 |
39 | const buttonsDiv = (
40 | <>
41 |
42 | {buttonsGuest.map((props, idx) => (
43 | history.push(props.route)}>
48 | {props.title}
49 |
50 | ))}
51 |
52 | >
53 | )
54 | const menuIcon = (
55 | <>
56 | setIsMobileMenuVisible(!isMobileMenuVisible)}>
60 |
61 |
62 | >
63 | )
64 |
65 | const mobileMenu = (
66 | <>
67 |
68 |
69 |
73 | {buttonsGuest.map((props, idx) => (
74 | history.push(props.route)}>
78 | {' '}
79 | {props.title}
80 |
81 | ))}
82 |
83 |
84 |
85 | >
86 | )
87 | return (
88 | <>
89 |
90 |
96 | {isLarge ? buttonsDiv : menuIcon}
97 |
98 | {mobileMenu}
99 | >
100 | )
101 | }
102 |
103 | export default memo(Navbar)
104 |
--------------------------------------------------------------------------------
/src/components/Navbar/NavbarUser.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, memo, useRef } from 'react'
2 | import { connect } from 'react-redux'
3 | import { useHistory } from 'react-router-dom'
4 |
5 | import { Dispatch, bindActionCreators } from 'redux'
6 |
7 | import IconButton from '@material-ui/core/IconButton'
8 | import Toolbar from '@material-ui/core/Toolbar'
9 | import AccountCircleIcon from '@material-ui/icons/AccountCircle'
10 |
11 | import { setUserNone } from 'actions'
12 | import Logo from 'images/navbar_logo.svg'
13 | import { buildURI as buildHomePageURI } from 'pages/HomePage'
14 | import UserMenu from 'components/Navbar/UserMenu'
15 |
16 | const mapDispatchToProps = (dispatch: Dispatch) =>
17 | bindActionCreators({ setUserNone }, dispatch)
18 |
19 | const Navbar = () => {
20 | const [menuOpen, setMenuOpen] = useState(false)
21 | const anchorRef = useRef(null)
22 |
23 | const history = useHistory()
24 |
25 | const goHome = () => {
26 | if (history.location.pathname !== buildHomePageURI())
27 | history.push(buildHomePageURI())
28 | }
29 |
30 | const menuIcon = (
31 | <>
32 | setMenuOpen(true)}>
38 |
39 |
40 | >
41 | )
42 |
43 | return (
44 |
45 |
51 | {menuIcon}
52 |
57 |
58 | )
59 | }
60 |
61 | export default memo(connect(null, mapDispatchToProps)(Navbar))
62 |
--------------------------------------------------------------------------------
/src/components/Navbar/UserMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, bindActionCreators } from 'redux'
2 |
3 | import Menu from '@material-ui/core/Menu'
4 | import MenuItem from '@material-ui/core/MenuItem'
5 |
6 | import { setUserNone } from 'actions'
7 | import api from 'API'
8 | import SimpleConfirmationDialog from 'components/SimpleConfirmationDialog'
9 | import { useMySnackbar } from 'hooks'
10 | import { buildURI as buildProfilePageURI } from 'pages/ProfilePage'
11 | import { buildURI as buildAccountPageURI } from 'pages/SettingsPage'
12 | import { ConnectedProps, connect } from 'react-redux'
13 | import { useHistory } from 'react-router'
14 | import React, { useState } from 'react'
15 |
16 | const mapDispatchToProps = (dispatch: Dispatch) =>
17 | bindActionCreators({ setUserNone }, dispatch)
18 |
19 | const connector = connect(null, mapDispatchToProps)
20 | interface UserMenuProps extends ConnectedProps {
21 | open: boolean
22 | anchor: HTMLElement | null
23 | setOpen: React.Dispatch>
24 | }
25 |
26 | const UserMenu = ({ open, anchor, setOpen, setUserNone }: UserMenuProps) => {
27 | const history = useHistory()
28 | const notify = useMySnackbar()
29 | const [confirmationDialogOpen, setConfirmationDialogOpen] =
30 | useState(false)
31 |
32 | const menuStyle = {
33 | minWidth: '100px',
34 | }
35 | const handleLogout = () => {
36 | notify('Sessão encerrada', 'info')
37 | api.logout()
38 | setUserNone()
39 | }
40 | return (
41 | setOpen(false)}
45 | transformOrigin={{
46 | vertical: -60,
47 | horizontal: 'left',
48 | }}>
49 | history.push(buildProfilePageURI())}
51 | style={menuStyle}>
52 | {' '}
53 | Perfil{' '}
54 |
55 | history.push(buildAccountPageURI())}
57 | style={menuStyle}>
58 | {' '}
59 | Conta{' '}
60 |
61 | setConfirmationDialogOpen(true)}
63 | style={menuStyle}>
64 | {' '}
65 | Logout{' '}
66 |
67 | setConfirmationDialogOpen(false)}
73 | confirmCallback={handleLogout}
74 | />
75 |
76 | )
77 | }
78 | export default connector(UserMenu)
79 |
--------------------------------------------------------------------------------
/src/components/Navbar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, PropsWithChildren, ReactElement } from 'react'
2 | import { connect, ConnectedProps } from 'react-redux'
3 |
4 | import AppBar from '@material-ui/core/AppBar'
5 | import Slide from '@material-ui/core/Slide'
6 | import useScrollTrigger from '@material-ui/core/useScrollTrigger'
7 |
8 | import { AppState } from 'types/redux'
9 | import { unknownUser, guestUser } from 'types/User'
10 |
11 | import NavbarGuest from 'components/Navbar/NavbarGuest'
12 | import NavbarUser from 'components/Navbar/NavbarUser'
13 |
14 | const HideOnScroll: React.FC = ({ children }) => {
15 | const trigger = useScrollTrigger()
16 |
17 | return (
18 |
19 | {children as ReactElement}
20 |
21 | )
22 | }
23 |
24 | const mapStateToProps = (state: AppState) => ({ user: state.user })
25 | const connector = connect(mapStateToProps)
26 |
27 | type NavbarProps = ConnectedProps
28 |
29 | const Navbar = ({ user, ...props }: NavbarProps) => {
30 | return (
31 |
32 |
33 | {user === unknownUser || user === guestUser ? (
34 |
35 | ) : (
36 |
37 | )}
38 |
39 |
40 | )
41 | }
42 |
43 | export default memo(connector(Navbar))
44 |
--------------------------------------------------------------------------------
/src/components/Navbar/style.css:
--------------------------------------------------------------------------------
1 | .toolbar {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: space-between;
5 | }
6 |
7 | .h100 {
8 | height: 100%;
9 | }
10 |
11 |
12 | .w100 {
13 | width: 100%;
14 | }
--------------------------------------------------------------------------------
/src/components/PartialInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import InputBase from '@material-ui/core/InputBase'
4 | import { makeStyles } from '@material-ui/core/styles'
5 |
6 | const useStyles = makeStyles(theme => ({
7 | input: {
8 | fontFamily: 'Courier',
9 | [theme.breakpoints.up('sm')]: {
10 | fontSize: '18pt',
11 | margin: theme.spacing(1),
12 | },
13 | [theme.breakpoints.down('sm')]: {
14 | fontSize: '12pt',
15 | margin: '2px',
16 | },
17 | },
18 | }))
19 |
20 | interface PartialInputProps {
21 | id: number
22 | value: string
23 | handlePaste: (id: number, str: string) => void
24 | handleChange: (id: number, str: string) => void
25 | disabled?: boolean
26 | }
27 | const PartialInput: React.FC = ({
28 | id,
29 | value,
30 | handlePaste,
31 | handleChange,
32 | disabled = false,
33 | }) => {
34 | const classes = useStyles()
35 | const [focused, setFocused] = useState(false)
36 |
37 | const onChange = (
38 | evt: React.ChangeEvent,
39 | ) => {
40 | let str = evt.target.value
41 | str = str.replace(/-/g, '')
42 | if (/^\w*$/.test(str)) {
43 | const nativeEvent = evt.nativeEvent as InputEvent
44 | let inputType = ''
45 | if (nativeEvent instanceof InputEvent) {
46 | inputType = nativeEvent.inputType
47 | }
48 |
49 | if (inputType === 'insertFromPaste') {
50 | // allow to paste input
51 | handlePaste(id, str)
52 | } else {
53 | if (str.length <= 4) handleChange(id, str.toUpperCase())
54 | if (str.length === 4) {
55 | const next = document.querySelector(
56 | `#auth-code-${id + 1}`,
57 | )
58 | if (next) {
59 | next.focus()
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | const inputProps = {
67 | size: '4',
68 | style: {
69 | backgroundColor: disabled ? '#adadad' : '#F7F7F7',
70 | borderRadius: '2px 2px',
71 | boxShadow: focused ? 'inset 0 0 2px blue' : 'inset 0 0 2px #000000',
72 | padding: '5pt',
73 | },
74 | }
75 | return (
76 | ,
82 | ) => {
83 | evt.target.select()
84 | setFocused(true)
85 | }}
86 | onBlur={() => setFocused(false)}
87 | inputProps={inputProps}
88 | value={value}
89 | id={`auth-code-${id}`}
90 | onChange={evt => onChange(evt)}
91 | />
92 | )
93 | }
94 |
95 | export default PartialInput
96 |
--------------------------------------------------------------------------------
/src/components/PasswordInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import IconButton from '@material-ui/core/IconButton'
4 | import InputAdornment from '@material-ui/core/InputAdornment'
5 | import TextField from '@material-ui/core/TextField'
6 | import Visibility from '@material-ui/icons/Visibility'
7 | import VisibilityOff from '@material-ui/icons/VisibilityOff'
8 |
9 | const PasswordInput = (props: any) => {
10 | const [visible, setVisible] = useState(false)
11 |
12 | const handleClickShowPassword = () => {
13 | setVisible(!visible)
14 | }
15 |
16 | const handleMouseDownPassword = (
17 | event: React.MouseEvent,
18 | ) => {
19 | event.preventDefault()
20 | }
21 |
22 | return (
23 |
29 |
35 | {visible ? : }
36 |
37 |
38 | ),
39 | }}
40 | />
41 | )
42 | }
43 |
44 | export default PasswordInput
45 |
--------------------------------------------------------------------------------
/src/components/PasswordRedefinitionModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Dialog from '@material-ui/core/Dialog'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import DialogTitle from '@material-ui/core/DialogTitle'
8 | import Grid from '@material-ui/core/Grid'
9 | import TextField from '@material-ui/core/TextField'
10 | import Typography from '@material-ui/core/Typography'
11 |
12 | import api from 'API'
13 | import { useErrorDialog, useMySnackbar } from 'hooks'
14 | import { validateEmail } from 'utils'
15 |
16 | interface PasswordRedefinitionModalProps {
17 | open: boolean
18 | handleClose: () => void
19 | }
20 |
21 | const PasswordRedefinitionModal: React.FC = ({
22 | open,
23 | handleClose,
24 | }) => {
25 | const [email, setEmail] = useState('')
26 | const [showEmailError, setShowEmailError] = useState(false)
27 | const emailOk = !showEmailError || validateEmail(email) || !open
28 | const notify = useMySnackbar()
29 | const uspyAlert = useErrorDialog()
30 |
31 | const sendEmail = () => {
32 | // send activation email
33 | api.sendPasswordRedefinitionEmail(email)
34 | .then(() => {
35 | notify('Email enviado com sucesso', 'success')
36 | handleClose()
37 | })
38 | .catch(err => {
39 | console.error(err)
40 | uspyAlert(
41 | `Algo deu errado (${err.message}). Tente novamente mais tarde.`,
42 | 'Falha na redefinição',
43 | )
44 | })
45 | }
46 |
47 | return (
48 |
49 | Redefinir senha
50 |
51 |
52 |
53 |
54 | Insira seu email USP cadastrado para que enviemos um
55 | link de redefinição de senha!
56 |
57 |
58 |
59 | setShowEmailError(true)}
68 | onChange={(evt: any) => setEmail(evt.target.value)}
69 | variant="outlined"
70 | color="secondary"
71 | size="small"
72 | fullWidth
73 | />
74 |
75 |
76 |
77 |
78 |
85 | Enviar
86 |
87 |
88 |
89 | )
90 | }
91 |
92 | export default PasswordRedefinitionModal
93 |
--------------------------------------------------------------------------------
/src/components/SendActivationEmailModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Dialog from '@material-ui/core/Dialog'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import DialogTitle from '@material-ui/core/DialogTitle'
8 | import Grid from '@material-ui/core/Grid'
9 | import TextField from '@material-ui/core/TextField'
10 | import Typography from '@material-ui/core/Typography'
11 |
12 | import api from 'API'
13 | import { useMySnackbar, useErrorDialog } from 'hooks'
14 | import { validateEmail } from 'utils'
15 |
16 | interface SendActivationEmailModalProps {
17 | open: boolean
18 | handleClose: () => void
19 | }
20 |
21 | const SendActivationEmailModal: React.FC = ({
22 | open,
23 | handleClose,
24 | }) => {
25 | const [email, setEmail] = useState('')
26 | const [showEmailError, setShowEmailError] = useState(false)
27 | const emailOk = !showEmailError || validateEmail(email) || !open
28 | const notify = useMySnackbar()
29 | const uspyAlert = useErrorDialog()
30 |
31 | const sendEmail = () => {
32 | // send activation email
33 | api.sendActivationEmail(email)
34 | .then(() => {
35 | notify('Email enviado com sucesso', 'success')
36 | handleClose()
37 | })
38 | .catch(err => {
39 | console.error(err)
40 | uspyAlert(
41 | `Algo deu errado ${err.message}. Tente novamente mais tarde.`,
42 | 'Falha na ativação',
43 | )
44 | })
45 | }
46 |
47 | return (
48 |
49 | Verificar conta
50 |
51 |
52 |
53 |
54 | Insira seu email USP para que reenviemos um link de
55 | verificação da conta!
56 |
57 |
58 |
59 | setShowEmailError(true)}
68 | onChange={(evt: any) => setEmail(evt.target.value)}
69 | variant="outlined"
70 | color="secondary"
71 | size="small"
72 | fullWidth
73 | />
74 |
75 |
76 |
77 |
78 |
85 | Enviar
86 |
87 |
88 |
89 | )
90 | }
91 |
92 | export default SendActivationEmailModal
93 |
--------------------------------------------------------------------------------
/src/components/SimpleConfirmationDialog/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Dialog from '@material-ui/core/Dialog'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import DialogContentText from '@material-ui/core/DialogContentText'
8 | import DialogTitle from '@material-ui/core/DialogTitle'
9 | import PaperComponent from '@material-ui/core/Paper'
10 |
11 | interface SimpleConfirmationDialogProps {
12 | title: string
13 | body?: string
14 | cancelText: string
15 | confirmText: string
16 | open: boolean
17 | confirmCallback?: () => void
18 | cancelCallback?: () => void
19 | }
20 |
21 | const SimpleConfirmationDialog: React.FC = ({
22 | title,
23 | body,
24 | cancelText,
25 | confirmText,
26 | open,
27 | confirmCallback,
28 | cancelCallback,
29 | }) => {
30 | return (
31 |
35 | {title}
36 | {body ? (
37 |
38 | {body}
39 |
40 | ) : null}
41 |
42 |
43 | {cancelText}
44 |
45 |
46 | {confirmText}
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default SimpleConfirmationDialog
54 |
--------------------------------------------------------------------------------
/src/components/SubjectSiblings/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useState } from 'react'
2 | import api from 'API'
3 | import { SubjectSibling } from 'types/Subject'
4 | import { Button, CircularProgress, Grid, Tooltip } from '@material-ui/core'
5 | import { getInitials } from 'utils'
6 | import { buildURI as buildSubjectPageURI } from 'pages/SubjectPage'
7 |
8 | import { useHistory } from 'react-router'
9 |
10 | interface SubjectSiblingsProps {
11 | code: string
12 | course: string
13 | specialization: string
14 | }
15 |
16 | interface SiblingProps {
17 | code: string
18 | name: string
19 | optional: boolean
20 | course: string
21 | specialization: string
22 | }
23 |
24 | const Sibling = ({
25 | code,
26 | name,
27 | optional,
28 | course,
29 | specialization,
30 | }: SiblingProps) => {
31 | const history = useHistory()
32 |
33 | return (
34 |
38 | {
44 | history.push(
45 | buildSubjectPageURI(course, specialization, code),
46 | )
47 | }}>
48 | {name.length > 50 ? getInitials(name) : name}
49 |
50 |
51 | )
52 | }
53 |
54 | const SubjectSiblings: React.FC = ({
55 | code,
56 | course,
57 | specialization,
58 | }) => {
59 | const [isLoading, setIsLoading] = useState(true)
60 | const [siblings, setSiblings] = useState([])
61 |
62 | useEffect(() => {
63 | api.getSubjectSiblings(code, course, specialization)
64 | .then(res => {
65 | setSiblings(res.sort((a, b) => a.name.length - b.name.length))
66 | setIsLoading(false)
67 | })
68 | .catch(err => {
69 | console.error(err)
70 | })
71 | }, [code, course, specialization])
72 |
73 | if (isLoading) {
74 | return (
75 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 |
83 | return (
84 |
85 | {siblings.map(sibling => (
86 |
87 |
94 |
95 | ))}
96 | {' '}
97 |
98 | )
99 | }
100 |
101 | export default memo(SubjectSiblings)
102 |
--------------------------------------------------------------------------------
/src/components/UpdateTranscriptModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useDispatch } from 'react-redux'
3 |
4 | import Button from '@material-ui/core/Button'
5 | import CircularProgress from '@material-ui/core/CircularProgress'
6 | import Dialog from '@material-ui/core/Dialog'
7 | import DialogActions from '@material-ui/core/DialogActions'
8 | import DialogContent from '@material-ui/core/DialogContent'
9 | import DialogTitle from '@material-ui/core/DialogTitle'
10 | import Grid from '@material-ui/core/Grid'
11 | import Typography from '@material-ui/core/Typography'
12 | import useMediaQuery from '@material-ui/core/useMediaQuery'
13 | import InfoIcon from '@material-ui/icons/InfoOutlined'
14 | import useTheme from '@material-ui/core/styles/useTheme'
15 |
16 | import { setLastUpdatedAccount } from 'actions'
17 | import api from 'API'
18 | import InfoModal from 'components/InfoModal'
19 | import PartialInput from 'components/PartialInput'
20 | import { useErrorDialog, useMySnackbar } from 'hooks'
21 |
22 | interface PropsType {
23 | open: boolean
24 | close: () => void
25 | }
26 |
27 | const UpdateTranscriptModal: React.FC = ({ open, close }) => {
28 | const [authCode, setAuthCode] = useState(['', '', '', ''])
29 | const [infoModalOpen, setInfoModalOpen] = useState(false)
30 | const [pending, setPending] = useState(false)
31 |
32 | const theme = useTheme()
33 | const isDesktop = useMediaQuery(theme.breakpoints.up('sm'))
34 | const notify = useMySnackbar()
35 |
36 | const authCodeString = authCode
37 | .reduce((prev, cur) => prev + '-' + cur, '')
38 | .substring(1)
39 | const authCodeOk = /^[\w\d]{4}-[\w\d]{4}-[\w\d]{4}-[\w\d]{4}/.test(
40 | authCodeString,
41 | )
42 |
43 | const uspyAlert = useErrorDialog()
44 |
45 | const handleAuthCodePaste = (id: number, str: string) => {
46 | const values = authCode.slice()
47 | for (let i = id; i < 4; ++i) values[i] = ''
48 | for (let i = 0; i < str.length; ++i) {
49 | if (id + Math.floor(i / 4) >= 4) break
50 | values[id + Math.floor(i / 4)] += str[i]
51 | }
52 | setAuthCode(values)
53 | }
54 |
55 | const handleAuthCodeChange = (id: number, str: string) => {
56 | const values = authCode.slice()
57 | values[id] = str
58 | setAuthCode(values)
59 | }
60 |
61 | const dispatch = useDispatch()
62 | const submit = () => {
63 | setPending(true) // update is pending
64 | api.updateAccount(authCodeString)
65 | .then(() => {
66 | notify('Histórico atualizado com sucesso', 'success')
67 | setPending(false)
68 | close()
69 |
70 | api.isAuthenticated()
71 | .then(([_, lastUpdated]) => {
72 | dispatch(setLastUpdatedAccount(lastUpdated))
73 | })
74 | .catch(err => {
75 | console.error(`Error: (${err})`)
76 | })
77 | })
78 | .catch(err => {
79 | if (err.code === 'bad_request') {
80 | uspyAlert(
81 | 'Código de autenticidade ou captcha inválidos. Lembre-se que o código de autenticidade usado deve ter sido gerado na última hora!',
82 | )
83 | } else {
84 | uspyAlert(
85 | `Algo deu errado (${err.message}). Tente novamente mais tarde`,
86 | 'Falha no cadastro',
87 | )
88 | }
89 | setPending(false)
90 | })
91 | }
92 |
93 | return (
94 |
95 | Atualizar histórico
96 |
97 |
102 |
103 |
104 | Atualize seu histórico obtendo um novo código de
105 | autenticidade do seu resumo escolar.
106 | setInfoModalOpen(true)}
110 | />
111 |
112 |
113 |
114 |
119 | {authCode.map((val, idx) => (
120 |
121 | {idx ? '-' : }
122 |
128 |
129 | ))}
130 |
131 |
132 |
133 |
134 |
135 |
136 | Cancelar
137 |
138 | : null}>
143 | Confirmar
144 |
145 |
146 | setInfoModalOpen(false)}
149 | />
150 |
151 | )
152 | }
153 |
154 | export default UpdateTranscriptModal
155 |
--------------------------------------------------------------------------------
/src/components/VoteButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import PlayArrowIcon from '@material-ui/icons/PlayArrow'
4 |
5 | interface PropsType {
6 | selected: boolean
7 | setSelected: () => void
8 | orientation?: 'up' | 'down' | 'left' | 'right'
9 | disabled?: boolean
10 | }
11 |
12 | const ORIENTATION_TO_DEGREE = {
13 | right: 0,
14 | up: 90,
15 | left: 180,
16 | down: 270,
17 | }
18 |
19 | const VoteButton: React.FC = ({
20 | selected,
21 | setSelected,
22 | orientation = 'right',
23 | disabled = 'false',
24 | }) => {
25 | const style = {
26 | transform: `rotate(-${ORIENTATION_TO_DEGREE[orientation]}deg)`,
27 | cursor: disabled ? 'auto' : 'pointer',
28 | }
29 |
30 | return (
31 |
37 | )
38 | }
39 |
40 | export default VoteButton
41 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingApprovalDonut/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | import Paper from '@material-ui/core/Paper'
4 | import Popover from '@material-ui/core/Popover'
5 |
6 | import { PieChart, Pie, ResponsiveContainer, Cell } from 'recharts'
7 |
8 | interface PropsType {
9 | approval: number
10 | neutral: number
11 | disapproval: number
12 | noStatsMessage?: string
13 | showQuestionMark?: boolean
14 | }
15 |
16 | function toPercentage(x: number): string {
17 | return (100 * x).toFixed(0) + '%'
18 | }
19 |
20 | const OfferingApprovalDonut: React.FC = ({
21 | approval,
22 | neutral,
23 | disapproval,
24 | noStatsMessage = 'Não existem avaliações suficientes para este professor',
25 | showQuestionMark = false,
26 | }: PropsType) => {
27 | let missingData = false
28 | let errorMessage = ''
29 | if (!approval && !neutral && !disapproval) {
30 | missingData = true
31 | neutral = 1.0
32 | approval = disapproval = 0.0
33 | } else if (approval + neutral + disapproval !== 1) {
34 | errorMessage = 'Dados incoerentes'
35 | neutral = 1.0
36 | approval = disapproval = 0.0
37 | console.error('Statistics of approval must sum up to 1')
38 | }
39 |
40 | // Popover (tooltip) stuff
41 | const [anchorEl, setAnchorEl] = useState(null)
42 | const handlePopoverOpen = (
43 | event: React.MouseEvent,
44 | ) => {
45 | setAnchorEl(event.currentTarget)
46 | }
47 | const handlePopoverClose = () => {
48 | setAnchorEl(null)
49 | }
50 |
51 | useEffect(() => {
52 | const handleScroll = () => handlePopoverClose()
53 | window.addEventListener('scroll', handleScroll)
54 |
55 | return () => {
56 | window.removeEventListener('scroll', handleScroll)
57 | }
58 | }, [])
59 |
60 | const isPopoverOpen = Boolean(anchorEl)
61 | const popover = (
62 |
80 |
81 | {errorMessage ||
82 | (missingData ? (
83 | noStatsMessage
84 | ) : (
85 |
86 | Aprovam: {toPercentage(approval)}
87 | Desaprovam: {toPercentage(disapproval)}
88 | Neutros: {toPercentage(neutral)}
89 |
90 | ))}
91 |
92 |
93 | )
94 |
95 | const data = [
96 | {
97 | value: approval,
98 | name: 'approval',
99 | },
100 | {
101 | value: neutral,
102 | name: 'neutral',
103 | },
104 | {
105 | value: disapproval,
106 | name: 'disapproval',
107 | },
108 | ]
109 |
110 | const colors = ['#00910E', '#6a86a3', '#FF0000']
111 |
112 | const wrapperStyle: React.CSSProperties = {
113 | display: 'flex',
114 | justifyContent: 'center',
115 | alignItems: 'center',
116 | position: 'relative',
117 | }
118 |
119 | return (
120 | <>
121 |
125 |
126 |
127 |
136 | {data.map((_, index) => (
137 | |
145 | ))}
146 |
147 |
148 |
149 | {showQuestionMark ? (
150 |
151 | ?
152 |
153 | ) : null}
154 |
155 | {popover}
156 | >
157 | )
158 | }
159 |
160 | export default React.memo(OfferingApprovalDonut)
161 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingEmotesSelector/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Grid from '@material-ui/core/Grid'
4 | import useTheme from '@material-ui/core/styles/useTheme'
5 | import Typography from '@material-ui/core/Typography'
6 | import useMediaQuery from '@material-ui/core/useMediaQuery'
7 |
8 | import EmoteHated from 'images/hated.svg'
9 | import EmoteIndifferent from 'images/indifferent.svg'
10 | import EmoteLiked from 'images/liked.svg'
11 | import EmoteLoved from 'images/loved.svg'
12 | import EmoteUnliked from 'images/unliked.svg'
13 |
14 | const emotes = [
15 | {
16 | emote: EmoteHated,
17 | caption: 'Odiei',
18 | },
19 | {
20 | emote: EmoteUnliked,
21 | caption: 'Não gostei',
22 | },
23 | {
24 | emote: EmoteIndifferent,
25 | caption: 'Indiferente',
26 | },
27 | {
28 | emote: EmoteLiked,
29 | caption: 'Gostei',
30 | },
31 | {
32 | emote: EmoteLoved,
33 | caption: 'Amei',
34 | },
35 | ]
36 |
37 | interface PropsType {
38 | rate: number
39 | setRate: (rate: number) => void
40 | isLocked: boolean
41 | compact?: boolean
42 | }
43 |
44 | const OfferingEmotesSelector: React.FC = ({
45 | rate,
46 | setRate,
47 | isLocked,
48 | compact = true,
49 | }) => {
50 | const theme = useTheme()
51 | const isDesktop = useMediaQuery(theme.breakpoints.up('sm'))
52 |
53 | return (
54 |
61 | {emotes.map((emote, idx) => (
62 |
63 |
68 |
76 |
setRate(idx + 1)
90 | }
91 | />
92 |
93 |
100 | {emote.caption}
101 |
102 |
103 |
104 | ))}
105 |
106 | )
107 | }
108 |
109 | export default OfferingEmotesSelector
110 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingReviewInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import './style.css'
4 |
5 | interface PropsType {
6 | content: string
7 | onChange: (s: string) => void
8 | limit: number
9 | rows: number
10 | placeholder: string
11 | disabled: boolean
12 | offering: string
13 | savedComment: string
14 | pendingSavedComment: boolean
15 | }
16 |
17 | const OfferingReviewInput: React.FC = ({
18 | content,
19 | limit,
20 | rows,
21 | onChange,
22 | placeholder,
23 | disabled,
24 | savedComment,
25 | pendingSavedComment,
26 | }) => {
27 | const ref1 = React.useRef(null)
28 | const ref2 = React.useRef(null)
29 |
30 | const style = {
31 | height: `${1.1876 * rows}em`,
32 | }
33 |
34 | const update = () => {
35 | onChange(ref1.current.innerText)
36 | ref2.current.scrollTop = ref1.current.scrollTop
37 | }
38 |
39 | /* The following useMemo is a hack (aka gambiarra) used to not make the component
40 | be re-rendered on every change of its content,
41 | */
42 |
43 | const editorDiv = useMemo(() => {
44 | const body = pendingSavedComment ? '' : savedComment
45 | return (
46 |
53 | {body}
54 |
55 | )
56 | }, [pendingSavedComment, savedComment, disabled])
57 |
58 | return (
59 |
62 | {editorDiv}
63 |
64 | {content.substr(0, limit)}
65 | {content.substr(limit)}
66 |
67 |
68 | {content.length || disabled ? '' : placeholder}
69 |
70 |
71 | {content.length > limit ? (
72 | {content.length}
73 | ) : (
74 | content.length
75 | )}
76 | /{limit}
77 |
78 |
79 | )
80 | }
81 |
82 | export default OfferingReviewInput
83 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingReviewInput/style.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .offering-review-input-wrapper {
4 | border: 1px solid #e2e2e1;
5 | padding: 1rem;
6 | border-radius: 10px;
7 | position: relative;
8 |
9 | font-family: Roboto, Helvetica, Arial, sans-serif;
10 | font-size: 1rem;
11 | font-stretch: 100%;
12 | font-weight: 400;
13 | color: rgba(0, 0, 0, 0.87);
14 |
15 | line-height: 1.1876em;
16 | letter-spacing: 0.15008px;
17 |
18 | background-color: #f6f6f6;
19 | width: 100%;
20 | box-sizing: border-box;
21 | }
22 |
23 |
24 | .offering-review-input-wrapper div {
25 | box-sizing: border-box;
26 | outline: none;
27 | overflow-y: scroll;
28 |
29 | position: relative;
30 | text-transform: none;
31 | text-indent: 0px;
32 | text-shadow: none;
33 | text-align: start;
34 | appearance: auto;
35 | -webkit-rtl-ordering: logical;
36 | user-select: text;
37 | column-count: initial !important;
38 | word-spacing: normal;
39 | white-space: pre-wrap;
40 | overflow-wrap: break-word;
41 |
42 | -ms-overflow-style: none; /* IE and Edge */
43 | scrollbar-width: none; /* Firefox */
44 | z-index: 2;
45 | }
46 |
47 | .offering-review-input-wrapper div[contenteditable=true] {
48 | cursor: text;
49 | -webkit-writing-mode: horizontal-tb !important;
50 | writing-mode: horizontal-tb !important;
51 | -webkit-user-modify: read-write-plaintext-only;
52 | }
53 |
54 |
55 | .offering-review-input-wrapper div::-webkit-scrollbar {
56 | display: none;
57 | }
58 |
59 | .offering-review-input-wrapper div.highlight-mirror {
60 | min-width: 0;
61 | vertical-align: top;
62 |
63 | position: absolute;
64 |
65 | top: 1rem;
66 | left: 1rem;
67 | right: 1rem;
68 | bottom: 1rem;
69 |
70 | color: transparent !important;
71 |
72 | -webkit-touch-callout: none; /* iOS Safari */
73 | -webkit-user-select: none; /* Safari */
74 | -khtml-user-select: none; /* Konqueror HTML */
75 | -moz-user-select: none; /* Old versions of Firefox */
76 | -ms-user-select: none; /* Internet Explorer/Edge */
77 | user-select: none;
78 | z-index: 1;
79 | }
80 |
81 | .offering-review-input-wrapper div.placeholder {
82 | position: absolute;
83 |
84 | top: 1rem;
85 | left: 1rem;
86 | right: 1rem;
87 | bottom: 1rem;
88 |
89 | color: rgba(0, 0, 0, 0.753);
90 | -webkit-touch-callout: none; /* iOS Safari */
91 | -webkit-user-select: none; /* Safari */
92 | -khtml-user-select: none; /* Konqueror HTML */
93 | -moz-user-select: none; /* Old versions of Firefox */
94 | -ms-user-select: none; /* Internet Explorer/Edge */
95 | user-select: none;
96 |
97 | z-index: 1;
98 | }
99 |
100 | .offering-review-input-wrapper div.highlight-mirror::-webkit-scrollbar {
101 | display: none;
102 | }
103 |
104 |
105 | .offering-review-input-wrapper div.highlight-mirror span {
106 | background-color: rgb(251, 159, 168);
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingReviewReportDialog/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useContext, useState } from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Dialog from '@material-ui/core/Dialog'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import DialogContentText from '@material-ui/core/DialogContentText'
8 | import DialogTitle from '@material-ui/core/DialogTitle'
9 | import { withStyles } from '@material-ui/core/styles'
10 | import TextField from '@material-ui/core/TextField'
11 |
12 | import api from 'API'
13 | import OfferingContext from 'contexts/OfferingContext'
14 | import { useMySnackbar } from 'hooks'
15 |
16 | interface PropsType {
17 | review: string
18 | isOpen: boolean
19 | close: () => void
20 | }
21 |
22 | const ReportField = withStyles(theme => ({
23 | root: {
24 | border: '1px solid #e2e2e1',
25 | overflow: 'hidden',
26 | borderRadius: 10,
27 | backgroundColor: '#fcfcfb',
28 | transition: theme.transitions.create(['border-color', 'box-shadow']),
29 | padding: '.5rem',
30 | boxSizing: 'border-box',
31 | '&:hover': {
32 | backgroundColor: '#fff',
33 | },
34 | },
35 | }))(TextField)
36 |
37 | const COMMENT_THRESHOLD = 10
38 | const COMMENT_LIMIT = 300
39 |
40 | const OfferingReviewReportDialog: React.FC = ({
41 | review,
42 | isOpen,
43 | close,
44 | }) => {
45 | const [comment, setComment] = useState('')
46 | const { course, specialization, code, professor } =
47 | useContext(OfferingContext)
48 | const notify = useMySnackbar()
49 |
50 | const handleSubmit = useCallback(() => {
51 | // fazer alguma coisa com api
52 | api.reportOfferingReview(
53 | course,
54 | specialization,
55 | code,
56 | professor,
57 | review,
58 | comment,
59 | )
60 | .then(() => {
61 | notify('Sua denúncia foi enviada', 'success')
62 | close()
63 | })
64 | .catch(err => {
65 | notify('Algo deu errado, tente novamente mais tarde', 'error')
66 | console.error(err)
67 | })
68 | }, [comment])
69 |
70 | const handleCommentChange = (s: string) => {
71 | if (s.length <= COMMENT_LIMIT) {
72 | setComment(s)
73 | }
74 | }
75 |
76 | return (
77 |
78 | Reportar comentário
79 |
80 |
81 | Por favor, descreva por que esse comentário deve ser
82 | reportado. Caso ele seja ofensivo, preconceituoso,
83 | calunioso, irrelevante ao oferecimento ou, por qualquer
84 | outra razão, não deva estar aqui, nós tomaremos providências
85 | deletando o comentário e possivelmente banindo a conta do
86 | autor(a).
87 |
88 | handleCommentChange(evt.target.value)}
91 | fullWidth
92 | multiline
93 | InputProps={{ disableUnderline: true }}
94 | helperText={`${comment.length}/300`}
95 | rows={3}
96 | />
97 |
98 |
99 |
100 | Cancelar
101 |
102 |
106 | Enviar
107 |
108 |
109 |
110 | )
111 | }
112 |
113 | export default OfferingReviewReportDialog
114 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingReviewsFeed/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from 'react'
2 |
3 | import Divider from '@material-ui/core/Divider'
4 | import Grid from '@material-ui/core/Grid'
5 | import Typography from '@material-ui/core/Typography'
6 | import Skeleton from '@material-ui/lab/Skeleton'
7 |
8 | import { OfferingReview } from 'types/Offering'
9 |
10 | // import mockReviews from '__mocks__/MockReviews'
11 | import api from 'API'
12 | import OfferingReviewBalloon from 'components/offerings/OfferingReviewBalloon'
13 | import OfferingContext from 'contexts/OfferingContext'
14 | import ReviewContext from 'contexts/ReviewContext'
15 |
16 | const SkeletonProgress = () => {
17 | return (
18 |
19 | {new Array(10).fill(0).map((_, idx) => (
20 |
26 |
33 |
39 |
40 |
46 |
47 |
48 |
52 |
53 |
54 | ))}
55 |
56 | )
57 | }
58 |
59 | const OfferingReviewsFeed = () => {
60 | const [loading, setLoading] = useState(true)
61 | const [reviews, setReviews] = useState(null)
62 | const { professor, course, specialization, code } =
63 | useContext(OfferingContext)
64 |
65 | const { userReview } = useContext(ReviewContext)
66 |
67 | useEffect(() => {
68 | setLoading(true)
69 | api.getOfferingReviews(course, specialization, code, professor)
70 | .then(reviews => {
71 | setLoading(false)
72 | setReviews(reviews)
73 | })
74 | .catch(err => {
75 | setLoading(false)
76 | if (err.code === 'not_found') {
77 | setReviews([])
78 | } else {
79 | console.error(err)
80 | }
81 | })
82 | }, [professor, userReview])
83 |
84 | if (loading || reviews === null) {
85 | return (
86 |
87 |
88 |
89 | )
90 | } else if (reviews?.length === 0 && userReview === null) {
91 | return (
92 |
98 |
99 |
100 | Nenhum comentário foi encontrado para este oferecimento.
101 |
102 |
103 |
104 |
105 | Seja o primeiro a avaliar!
106 |
107 |
108 |
109 | )
110 | } else {
111 | return (
112 |
113 |
118 | {userReview ? (
119 | <>
120 |
126 |
130 |
131 |
132 | >
133 | ) : null}
134 |
135 | {reviews.map(rev =>
136 | userReview?.uuid === rev.uuid ? null : (
137 |
144 |
145 |
146 | ),
147 | )}
148 |
149 |
150 | )
151 | }
152 | }
153 |
154 | export default OfferingReviewsFeed
155 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingReviewsPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from 'react'
2 |
3 | // import Grid from '@material-ui/core/Grid'
4 |
5 | import { OfferingReview } from 'types/Offering'
6 |
7 | import api from 'API'
8 | import OfferingReviewBox from 'components/offerings/OfferingReviewBox'
9 | import OfferingReviewsFeed from 'components/offerings/OfferingReviewsFeed'
10 | import OfferingContext from 'contexts/OfferingContext'
11 | import ReviewContext from 'contexts/ReviewContext'
12 | import { guestUser, unknownUser, User } from 'types/User'
13 |
14 | type PropsType = {
15 | user: User
16 | }
17 |
18 | const OfferingReviewsPanel: React.FC = ({ user }) => {
19 | const { professor, course, specialization, code } =
20 | useContext(OfferingContext)
21 | const [userReview, setUserReview] = useState(null)
22 | useEffect(() => {
23 | api.getUserOfferingReview(course, specialization, code, professor)
24 | .then(review => {
25 | setUserReview(review)
26 | })
27 | .catch(err => {
28 | if (err.code === 'not_found') {
29 | setUserReview(null)
30 | } else {
31 | console.error(err)
32 | }
33 | })
34 | }, [course, specialization, code, professor])
35 | return (
36 |
43 |
49 |
50 |
51 |
52 |
53 | )
54 | }
55 |
56 | export default OfferingReviewsPanel
57 |
--------------------------------------------------------------------------------
/src/components/offerings/OfferingsList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Chip from '@material-ui/core/Chip'
4 | import List from '@material-ui/core/List'
5 | import ListItem from '@material-ui/core/ListItem'
6 | import ListItemText from '@material-ui/core/ListItemText'
7 |
8 | import { Offering } from 'types/Offering'
9 |
10 | import OfferingApprovalDonut from 'components/offerings/OfferingApprovalDonut'
11 |
12 | interface PropsType {
13 | list: Offering[]
14 | selected: Offering | null
15 | setSelected: (o: Offering) => any
16 | maxWidth?: number | string
17 | noStatsMessage?: string
18 | secondary?: string
19 | showQuestionMark?: boolean
20 | }
21 |
22 | function createYearsList(offering: Offering): React.ReactNode {
23 | return offering.years
24 | .sort((a: string, b: string) => {
25 | const aa = parseInt(a)
26 | const bb = parseInt(b)
27 | return aa === bb ? 0 : a > b ? -1 : 1
28 | })
29 | .slice(0, 5)
30 | .map(year => (
31 |
39 | ))
40 | }
41 |
42 | const OfferingsList: React.FC = ({
43 | list,
44 | selected,
45 | setSelected,
46 | maxWidth,
47 | noStatsMessage,
48 | secondary,
49 | showQuestionMark = false,
50 | }: PropsType) => {
51 | return (
52 |
53 | {list.map(offering => (
54 | setSelected(offering)}>
60 |
64 | {!secondary
65 | ? createYearsList(offering)
66 | : secondary}
67 | >
68 | }
69 | />
70 |
77 |
78 | ))}
79 |
80 | )
81 | }
82 |
83 | export default OfferingsList
84 |
--------------------------------------------------------------------------------
/src/components/profile/VoteButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | import IconButton from '@material-ui/core/IconButton'
4 | import Tooltip from '@material-ui/core/Tooltip'
5 | import ThumbDownIcon from '@material-ui/icons/ThumbDown'
6 | import ThumbDownOutlinedIcon from '@material-ui/icons/ThumbDownOutlined'
7 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'
8 | import ThumbUpOutlinedIcon from '@material-ui/icons/ThumbUpOutlined'
9 | import Skeleton from '@material-ui/lab/Skeleton'
10 |
11 | import { Record } from 'types/Record'
12 | import { SubjectReview } from 'types/Subject'
13 |
14 | import api from 'API'
15 |
16 | interface VoteButtonPropsType {
17 | type: 'up' | 'down'
18 | filled: boolean
19 | onClick: () => void
20 | color?: 'primary' | 'secondary'
21 | size?: 'default' | 'small' | 'large'
22 | }
23 |
24 | const ICONS = [
25 | [ThumbDownOutlinedIcon, ThumbDownIcon],
26 | [ThumbUpOutlinedIcon, ThumbUpIcon],
27 | ]
28 |
29 | export const VoteButton: React.FC = ({
30 | type,
31 | filled,
32 | onClick,
33 | color = 'primary',
34 | size = 'small',
35 | }) => {
36 | const Icon = ICONS[+(type === 'up')][+filled]
37 |
38 | return (
39 |
42 |
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | interface VoteButtonGroupPropsType {
50 | record: Record
51 | color?: 'primary' | 'secondary'
52 | size?: 'default' | 'small' | 'large'
53 | }
54 |
55 | export const VoteButtonGroup: React.FC = ({
56 | record,
57 | color = 'primary',
58 | size = 'small',
59 | }) => {
60 | const [subjectReview, setSubjectReview] = useState(
61 | null,
62 | )
63 | const [pending, setPending] = useState(true)
64 |
65 | const { course, specialization, code } = record
66 |
67 | useEffect(() => {
68 | api.getSubjectReview(course, specialization, code)
69 | .then(rev => {
70 | setSubjectReview(rev)
71 | setPending(false)
72 | })
73 | .catch(err => {
74 | if (err.code !== 'not_found') {
75 | // either user is not logged in or user
76 | // was not enrolled in subject, so this should never happen
77 | console.error(err)
78 | }
79 | setPending(false)
80 | })
81 | }, [record])
82 |
83 | const handleReviewSubject = (rev: 'up' | 'down') => {
84 | const review: SubjectReview = {
85 | categories: {
86 | worth_it: rev === 'up',
87 | },
88 | }
89 | api.makeSubjectReview(course, specialization, code, review)
90 | setSubjectReview(review)
91 | }
92 |
93 | if (pending) {
94 | return
95 | }
96 |
97 | return (
98 | <>
99 | {['down', 'up'].map(type => (
100 | handleReviewSubject(type as 'up' | 'down')}
109 | />
110 | ))}
111 | >
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/src/contexts/OfferingContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface OfferingContextType {
4 | professor: string
5 | course: string
6 | specialization: string
7 | code: string
8 | }
9 |
10 | const OfferingContext = React.createContext(null)
11 |
12 | export default OfferingContext
13 |
--------------------------------------------------------------------------------
/src/contexts/ReviewContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { OfferingReview } from 'types/Offering'
4 |
5 | interface ReviewContextType {
6 | userReview: OfferingReview
7 | setUserReview: (rev: OfferingReview) => void
8 | isGuest: boolean
9 | }
10 |
11 | const ReviewContext = React.createContext(null)
12 |
13 | export default ReviewContext
14 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { useLocation } from 'react-router-dom'
4 |
5 | import Slide from '@material-ui/core/Slide'
6 |
7 | import { uspyAlert } from 'actions'
8 | import { useSnackbar } from 'notistack'
9 |
10 | export const useMySnackbar = () => {
11 | const { enqueueSnackbar } = useSnackbar()
12 |
13 | return function (message: string, type: 'success' | 'info' | 'error') {
14 | enqueueSnackbar(message, {
15 | variant: type,
16 | anchorOrigin: {
17 | vertical: 'bottom',
18 | horizontal: 'center',
19 | },
20 | autoHideDuration: 2000,
21 | TransitionComponent: Slide,
22 | })
23 | }
24 | }
25 |
26 | export const useQueryParam = () => {
27 | const { search } = useLocation()
28 |
29 | return useMemo(() => new URLSearchParams(search), [search])
30 | }
31 |
32 | export const useErrorDialog = () => {
33 | const dispatch = useDispatch()
34 |
35 | return function (message: string, title?: string) {
36 | dispatch(uspyAlert(message, title))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/images/GithubLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
18 |
19 |
21 | image/svg+xml
22 |
24 |
25 |
26 |
27 |
47 |
49 |
51 |
52 |
58 |
59 |
--------------------------------------------------------------------------------
/src/images/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/images/authenticityCode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/authenticityCode.png
--------------------------------------------------------------------------------
/src/images/checkboxes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/checkboxes.png
--------------------------------------------------------------------------------
/src/images/comment_stat_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/images/grade_stat_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/images/hated.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/images/hated2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/images/indifferent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/images/indifferent2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/indifferent2.png
--------------------------------------------------------------------------------
/src/images/liked2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/liked2.png
--------------------------------------------------------------------------------
/src/images/loved2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/images/offering_stat_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/images/reviews.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/search-teacher.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/images/subject_review.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/subject_review.gif
--------------------------------------------------------------------------------
/src/images/subject_stat_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/images/track_progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/track_progress.gif
--------------------------------------------------------------------------------
/src/images/unliked2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Projeto-USPY/uspy-frontend/83fa472bd7f3a849ff53c19d5629948cf0381fff/src/images/unliked2.png
--------------------------------------------------------------------------------
/src/images/user_stat_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/images/write-comment.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/pages/AboutPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react'
2 |
3 | import Container from '@material-ui/core/Container'
4 | import Grid from '@material-ui/core/Grid'
5 | import Link from '@material-ui/core/Link'
6 | import Paper from '@material-ui/core/Paper'
7 |
8 | import Navbar from 'components/Navbar'
9 | import Image1 from 'images/aboutpage1.svg'
10 | import Image2 from 'images/aboutpage2.svg'
11 | import Image3 from 'images/aboutpage3.svg'
12 | import Image4 from 'images/aboutpage4.svg'
13 |
14 | import './style.css'
15 |
16 | interface RowContent {
17 | title: string
18 | text: ReactElement
19 | image: string
20 | }
21 |
22 | const content: RowContent[] = [
23 | {
24 | title: 'O que é?',
25 | text: (
26 | <>
27 |
28 | O USPY é uma plataforma colaborativa onde alunos de
29 | graduação podem se informar sobre disciplinas, professores,
30 | oferecimentos e seu progresso acadêmico como um todo. O seu
31 | propósito principal é fornecer ferramentas que tornem tudo
32 | isso mais fácil.
33 |
34 | >
35 | ),
36 | image: Image1,
37 | },
38 | {
39 | title: 'Por que?',
40 | text: (
41 | <>
42 |
43 | Identificamos que as informações acadêmicas são distribuídas
44 | de maneira pouco intuitiva e muito desorganizada. O{' '}
45 | USPY vem para mudar tudo isso!
46 |
47 |
48 |
49 | Queremos simplificar a busca por esses dados e criar
50 | indicadores a partir das experiências dos nossos usuários
51 | como estudantes.{' '}
52 |
53 | >
54 | ),
55 | image: Image2,
56 | },
57 | {
58 | title: 'Posso me tornar um usuário?',
59 | text: (
60 | <>
61 |
62 | No momento qualquer aluno de graduação da USP consegue se
63 | cadastrar, mas algumas funcionalidades da plataforma só
64 | serão úteis para alunos do ICMC, pois ainda não possuímos
65 | suporte para os dados de outros institutos.
66 |
67 |
68 |
69 | Nosso objetivo primordial é construir um ambiente amigável e
70 | útil para todos, portanto muitas mudanças estão por vir e
71 | qualquer sugestão é muito bem vinda.
72 |
73 | >
74 | ),
75 | image: Image3,
76 | },
77 | {
78 | title: 'Quais dados são coletados?',
79 | text: (
80 | <>
81 |
82 | O USPY é uma ferramenta colaborativa e portanto
83 | muitas das suas funcionalidades só estão disponíveis para
84 | aqueles que se cadastrarem.
85 |
86 |
87 |
88 | Quando é feito o cadastro, o sistema coleta o número USP e
89 | as notas do estudante através de seu resumo escolar. Isso é
90 | necessário para que possamos criar o perfil do estudante e
91 | calcular os indicadores de cada disciplina. Nenhum outro
92 | dado é coletado e tudo é armazenado de maneira segura e com
93 | muito carinho.
94 |
95 |
96 |
97 | Além disso, buscaremos sempre o máximo de transparência: o
98 | projeto é totalmente open source e se encontra no{' '}
99 |
103 | GitHub
104 |
105 | . Qualquer dúvida ou colaboração será recebida com muito
106 | prazer.
107 |
108 | >
109 | ),
110 | image: Image4,
111 | },
112 | ]
113 |
114 | interface RowProps extends RowContent {
115 | imageLeft: boolean
116 | }
117 |
118 | const Row: React.FC = ({ title, text, image, imageLeft }) => {
119 | return (
120 |
121 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {title}
133 | {text}
134 |
135 |
136 |
137 | )
138 | }
139 |
140 | export function getMeta(): any {
141 | return {
142 | title: 'Sobre o USPY',
143 | description:
144 | 'Leia sobre por que o USPY foi criado e como se tornar um usuário.',
145 | robots: ['index', 'follow'],
146 | }
147 | }
148 |
149 | export function buildURI(): string {
150 | return '/sobre'
151 | }
152 |
153 | const AboutPage = () => {
154 | return (
155 |
156 |
157 |
158 |
159 | Sobre o USPY
160 |
161 |
162 | {content.map((item: RowContent, idx: number) => (
163 |
164 |
170 |
171 | ))}
172 |
173 |
174 |
175 | )
176 | }
177 |
178 | export default AboutPage
179 |
--------------------------------------------------------------------------------
/src/pages/AboutPage/style.css:
--------------------------------------------------------------------------------
1 | .about-block {
2 | min-height: 150px !important;
3 | padding: 20px !important;
4 | border-radius: 10px !important;
5 | text-align: justify !important;
6 |
7 | transition: all 0.2s ease !important;
8 | -webkit-transition: all 0.2s ease !important;
9 | }
10 |
11 | .about-block:hover {
12 | -webkit-transform: translateY(-1%) scale(1.01, 1.01);
13 | transform: translateY(-1%) scale(1.01, 1.01);
14 | background: rgb(228, 228, 228, 0.1);
15 | }
16 |
--------------------------------------------------------------------------------
/src/pages/AccountActivationPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useHistory } from 'react-router-dom'
3 |
4 | import CircularProgress from '@material-ui/core/CircularProgress'
5 | import Container from '@material-ui/core/Container'
6 | import Grid from '@material-ui/core/Grid'
7 | import Typography from '@material-ui/core/Typography'
8 |
9 | import api from 'API'
10 | import ErrorScreen from 'components/ErrorScreen'
11 | import Navbar from 'components/Navbar'
12 | import { buildURI as buildLoginPageURI } from 'pages/LoginPage'
13 |
14 | export function buildURI(): string {
15 | return '/account/verify'
16 | }
17 |
18 | const AccountActivationPage = () => {
19 | const [verifying, setVerifying] = useState(true)
20 | const [errorMessage, setErrorMessage] = useState(null)
21 | const history = useHistory()
22 |
23 | useEffect(() => {
24 | const token = new URLSearchParams(window.location.search).get('token') // get token from URL params
25 | api.verifyAccount(token)
26 | .then(() => {
27 | setVerifying(false)
28 | setTimeout(() => {
29 | history.replace(buildLoginPageURI())
30 | }, 3000)
31 | })
32 | .catch(err => {
33 | setVerifying(false)
34 | if (err.code === 'bad_request') {
35 | setErrorMessage('Erro: este link expirou :(')
36 | } else if (err.code === 'not_found') {
37 | setErrorMessage('Erro: este usuário não existe')
38 | } else {
39 | setErrorMessage(
40 | `Algo deu errado (${err.message}). Tente novamente mais tarde`,
41 | )
42 | }
43 | })
44 | }, [])
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 | {errorMessage ? (
53 |
54 | ) : (
55 |
56 |
61 |
62 | {verifying ? (
63 |
64 | ) : (
65 |
66 | {' '}
67 | Conta verificada com sucesso!{' '}
68 |
69 | )}
70 |
71 |
72 |
73 | )}
74 |
75 |
76 | )
77 | }
78 |
79 | export default AccountActivationPage
80 |
--------------------------------------------------------------------------------
/src/pages/HomePage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import { useHistory } from 'react-router-dom'
3 |
4 | import Container from '@material-ui/core/Container'
5 | import Fab from '@material-ui/core/Fab'
6 | import Grid from '@material-ui/core/Grid'
7 | import { useTheme } from '@material-ui/core/styles'
8 | import SvgIcon from '@material-ui/core/SvgIcon'
9 | import useMediaQuery from '@material-ui/core/useMediaQuery'
10 | import NavigationIcon from '@material-ui/icons/Navigation'
11 |
12 | import Footer from 'components/Footer'
13 | import GeneralSearch from 'components/GeneralSearch'
14 | import Landing from 'components/Landing'
15 | import Navbar from 'components/Navbar'
16 | import { buildURI as buildSubjectPageURI } from 'pages/SubjectPage'
17 |
18 | import './style.css'
19 | import Typography from '@material-ui/core/Typography'
20 |
21 | import logo from 'images/logo.svg'
22 | import ArrowDownIcon from 'images/arrow-down.svg?react'
23 |
24 | export function buildURI(): string {
25 | return '/'
26 | }
27 |
28 | export function getMeta(): any {
29 | return {
30 | title: 'USPY',
31 | description: `Procure por disciplinas e veja seus oferecimentos, requisitos, distribuição de médias, e muito mais.
32 | Avalie professores, veja seu histórico escolar, tudo em uma plataforma centralizada e fácil de usar!`,
33 | robots: ['index', 'follow'],
34 | }
35 | }
36 |
37 | const HomePage = () => {
38 | const theme = useTheme()
39 | const isLarge = useMediaQuery(theme.breakpoints.up('sm'))
40 |
41 | const history = useHistory()
42 | const clickItem = (
43 | courseCode: string,
44 | courseSpecialization: string,
45 | code: string,
46 | ) => {
47 | history.push(
48 | buildSubjectPageURI(courseCode, courseSpecialization, code),
49 | )
50 | }
51 |
52 | const seeMoreRef = useRef(null)
53 | // const [showSearch, setShowSearch] = useState(false)
54 | return (
55 | <>
56 |
61 |
62 |
63 |
64 |
65 |
72 |
78 |
79 |
87 |
88 |
89 |
90 |
96 |
97 | {
102 | seeMoreRef.current.scrollIntoView({
103 | behavior: 'smooth',
104 | })
105 | }}>
106 |
111 |
112 |
113 | Saber mais
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | {
129 | scrollTo({ left: 0, top: 0, behavior: 'smooth' })
130 | }}
131 | variant="circular"
132 | color="primary"
133 | style={{
134 | margin: 0,
135 | top: 'auto',
136 | right: 20,
137 | bottom: 20,
138 | left: 'auto',
139 | position: 'fixed',
140 | }}>
141 |
142 |
143 | >
144 | )
145 | }
146 |
147 | export default HomePage
148 |
--------------------------------------------------------------------------------
/src/pages/HomePage/style.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | height: 100%;
6 | }
7 |
8 | main {
9 | flex-grow: 1;
10 | }
11 |
12 | .other-links {
13 | width: 100%;
14 | display: flex;
15 | flex-direction: row-reverse;
16 | }
17 |
18 | @media screen and (min-width: 60em) {
19 | .other-links * {
20 | padding-left: 1rem;
21 | }
22 | }
23 |
24 | @media screen and (max-width: 60em) {
25 | .other-links {
26 | justify-content: space-around;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/pages/LoginPage/style.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | height: 100%;
6 | }
7 |
8 | main {
9 | flex-grow: 1;
10 | }
11 |
12 | .inputfield {
13 | width: 100%;
14 | margin-top: 2rem;
15 | }
--------------------------------------------------------------------------------
/src/pages/NotFoundPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Container from '@material-ui/core/Container'
4 | import Grid from '@material-ui/core/Grid'
5 | import Typography from '@material-ui/core/Typography'
6 |
7 | import Navbar from 'components/Navbar'
8 |
9 | import DetectiveImage from './detective.svg'
10 |
11 | const NotFoundPage = () => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 | {' '}
27 | Opss... Essa página não foi encontrada (404){' '}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default NotFoundPage
42 |
--------------------------------------------------------------------------------
/src/pages/OfferingsPage/Desktop.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useParams, useHistory } from 'react-router-dom'
3 |
4 | import Card from '@material-ui/core/Card'
5 | import CircularProgress from '@material-ui/core/CircularProgress'
6 | import Container from '@material-ui/core/Container'
7 | import Grid from '@material-ui/core/Grid'
8 | import { withStyles } from '@material-ui/core/styles'
9 | import Typography from '@material-ui/core/Typography'
10 |
11 | import { Offering } from 'types/Offering'
12 | import { Subject } from 'types/Subject'
13 |
14 | import Breadcrumb from 'components/Breadcrumb'
15 | import OfferingReviewsPanel from 'components/offerings/OfferingReviewsPanel'
16 | import OfferingsList from 'components/offerings/OfferingsList'
17 | import OfferingContext from 'contexts/OfferingContext'
18 | import { buildURI, getBreadcrumbLinks, URLParameter } from 'pages/OfferingsPage'
19 | import { User } from 'types/User'
20 |
21 | const GrayCard = withStyles({
22 | root: {
23 | backgroundColor: '#FAFAFA',
24 | },
25 | })(Card)
26 |
27 | interface PropsType {
28 | subject: Subject | null
29 | offerings: Offering[] | null
30 | selectedOffering: Offering | null
31 | user: User
32 | }
33 |
34 | const Desktop: React.FC = ({
35 | offerings,
36 | subject,
37 | selectedOffering,
38 | user,
39 | }) => {
40 | const { course, specialization, code } = useParams()
41 | const history = useHistory()
42 |
43 | const handleSelectOffering = (o: Offering) => {
44 | history.replace(buildURI(course, specialization, code, o.code))
45 | }
46 |
47 | const isLoading = subject === null || offerings === null
48 |
49 | return (
50 |
51 |
56 |
62 |
65 |
66 | {isLoading ? (
67 |
68 | {' '}
69 | {' '}
70 |
71 | ) : (
72 |
73 |
74 |
75 | {' '}
76 | Oferecimentos de {subject?.name}{' '}
77 |
78 |
79 |
80 |
86 |
87 |
92 |
98 |
103 |
104 |
105 |
106 |
107 |
111 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | )}
125 |
126 |
127 | )
128 | }
129 |
130 | export default Desktop
131 |
--------------------------------------------------------------------------------
/src/pages/OfferingsPage/Mobile.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useParams, useHistory } from 'react-router-dom'
3 |
4 | import Button from '@material-ui/core/Button'
5 | import CircularProgress from '@material-ui/core/CircularProgress'
6 | import Grid from '@material-ui/core/Grid'
7 | import Typography from '@material-ui/core/Typography'
8 |
9 | import { Offering } from 'types/Offering'
10 | import { Subject } from 'types/Subject'
11 |
12 | import Breadcrumb from 'components/Breadcrumb'
13 | import OfferingReviewsPanel from 'components/offerings/OfferingReviewsPanel'
14 | // import OfferingsList from 'components/Offerings/OfferingsList'
15 | import OfferingContext from 'contexts/OfferingContext'
16 | import { buildURI, getBreadcrumbLinks, URLParameter } from 'pages/OfferingsPage'
17 |
18 | import MobileOfferingSelector from './MobileOfferingSelector'
19 | import { User } from 'types/User'
20 |
21 | interface PropsType {
22 | subject: Subject | null
23 | offerings: Offering[] | null
24 | selectedOffering: Offering | null
25 | user: User
26 | }
27 |
28 | const Mobile: React.FC = ({
29 | offerings,
30 | subject,
31 | selectedOffering,
32 | user,
33 | }) => {
34 | const [mobileOfferingSelectorOpen, setMobileOfferingSelectorOpen] =
35 | useState(false)
36 |
37 | const { course, specialization, code } = useParams()
38 | const history = useHistory()
39 |
40 | const handleSelectOffering = (o: Offering) => {
41 | history.replace(buildURI(course, specialization, code, o.code))
42 | }
43 |
44 | const isLoading = subject === null || offerings === null
45 |
46 | return (
47 |
48 |
55 |
58 |
59 | {isLoading ? (
60 |
61 | {' '}
62 | {' '}
63 |
64 | ) : (
65 |
66 |
67 |
68 | {' '}
69 | Oferecimentos de {subject?.name}{' '}
70 |
71 |
72 |
73 |
77 |
78 |
79 |
80 | {selectedOffering?.professor}
81 |
82 |
83 |
84 |
89 | setMobileOfferingSelectorOpen(true)
90 | }>
91 | TROCAR
92 |
93 |
94 |
95 |
96 |
103 |
104 |
105 | {!mobileOfferingSelectorOpen ? null : (
106 |
109 | setMobileOfferingSelectorOpen(false)
110 | }
111 | selected={selectedOffering}
112 | setSelected={handleSelectOffering}
113 | offerings={offerings}
114 | />
115 | )}
116 |
117 |
118 |
119 |
120 | )}
121 |
122 | )
123 | }
124 |
125 | export default Mobile
126 |
--------------------------------------------------------------------------------
/src/pages/OfferingsPage/MobileOfferingSelector.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import Button from '@material-ui/core/Button'
4 | import Slide from '@material-ui/core/Slide'
5 |
6 | import { Offering } from 'types/Offering'
7 |
8 | import OfferingsList from 'components/offerings/OfferingsList'
9 | // import Grid from '@material-ui/core/Grid'
10 |
11 | interface PropsType {
12 | open: boolean
13 | close: () => void
14 | selected: Offering | null
15 | offerings: Offering[] | null
16 | setSelected: (o: Offering) => void
17 | }
18 |
19 | const MobileOfferingSelector: React.FC = ({
20 | open,
21 | close,
22 | offerings,
23 | selected,
24 | setSelected,
25 | }) => {
26 | const [isClosing, setIsClosing] = useState(false)
27 | const handleClose = () => {
28 | setIsClosing(true)
29 | setTimeout(() => close(), 500)
30 | }
31 | const handleSelect = (o: Offering) => {
32 | setSelected(o)
33 | handleClose()
34 | }
35 | return (
36 |
37 |
38 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
59 |
62 |
68 | CANCELAR
69 |
70 |
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | export default MobileOfferingSelector
78 |
--------------------------------------------------------------------------------
/src/pages/OfferingsPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { useParams } from 'react-router-dom'
3 |
4 | import Grid from '@material-ui/core/Grid'
5 | import { useTheme } from '@material-ui/core/styles'
6 | import useMediaQuery from '@material-ui/core/useMediaQuery'
7 |
8 | import { Offering } from 'types/Offering'
9 | import { Subject } from 'types/Subject'
10 |
11 | import api from 'API'
12 | import ErrorScreen from 'components/ErrorScreen'
13 | import Navbar from 'components/Navbar'
14 | import { useQueryParam } from 'hooks'
15 | import { buildURI as buildSubjectPageURI } from 'pages/SubjectPage'
16 | import { buildURI as buildSubjectsPageURI } from 'pages/SubjectsPage'
17 |
18 | import Desktop from './Desktop'
19 | import Mobile from './Mobile'
20 | import { AppState } from 'types/redux'
21 | import { ConnectedProps, connect } from 'react-redux'
22 |
23 | export interface URLParameter {
24 | course: string
25 | specialization: string
26 | code: string
27 | }
28 |
29 | export function buildURI(
30 | courseCode: string,
31 | courseSpecialization: string,
32 | subjectCode: string,
33 | professorCode?: string,
34 | ): string {
35 | return `/oferecimentos/${courseCode}/${courseSpecialization}/${subjectCode}${
36 | professorCode ? '?professor=' + professorCode : ''
37 | }`
38 | }
39 |
40 | export function getBreadcrumbLinks(
41 | course: string,
42 | specialization: string,
43 | code: string,
44 | ) {
45 | return [
46 | {
47 | url: buildSubjectsPageURI(course, specialization),
48 | text: 'Disciplinas',
49 | },
50 | {
51 | url: buildSubjectPageURI(course, specialization, code),
52 | text: code,
53 | },
54 | {
55 | url: buildURI(course, specialization, code),
56 | text: 'Oferecimentos',
57 | },
58 | ]
59 | }
60 |
61 | const mapStateToProps = (st: AppState) => ({ user: st.user })
62 | const connector = connect(mapStateToProps)
63 |
64 | type OfferingsPageProps = ConnectedProps
65 |
66 | const OfferingsPage = ({ user }: OfferingsPageProps) => {
67 | const theme = useTheme()
68 | const isDesktop = useMediaQuery(theme.breakpoints.up('sm'))
69 |
70 | const { course, specialization, code } = useParams()
71 | const [subject, setSubject] = useState(null)
72 | const [errorMessage, setErrorMessage] = useState('')
73 | const [offerings, setOfferings] = useState(null)
74 |
75 | const selectedOfferingCode: string = useQueryParam().get('professor') || ''
76 |
77 | useEffect(() => {
78 | api.getSubjectWithCourseAndCode(course, specialization, code)
79 | .then(data => {
80 | setSubject(data)
81 | })
82 | .catch(err => {
83 | if (err.code === 'not_found') {
84 | setErrorMessage(
85 | 'Não foi possível encontrar essa disciplina',
86 | )
87 | } else {
88 | setErrorMessage(
89 | `Algo deu errado (${err.message}). Tente novamente mais tarde`,
90 | )
91 | console.error(err)
92 | }
93 | })
94 |
95 | api.getSubjectOfferings(course, specialization, code)
96 | .then(data => {
97 | setOfferings(data)
98 | })
99 | .catch(err => {
100 | if (err.code === 'not_found') {
101 | setErrorMessage(
102 | 'Não foi possível encontrar oferecimentos para esta disciplina',
103 | )
104 | } else if (err.code === 'unauthorized') {
105 | setErrorMessage(
106 | 'Você deve estar logado para ver esta página!',
107 | )
108 | } else {
109 | setErrorMessage(
110 | `Algo deu errado (${err.message}). Tente novamente mais tarde`,
111 | )
112 | console.error(err)
113 | }
114 | })
115 | }, [])
116 |
117 | let selectedOffering: Offering = offerings?.find(
118 | o => o.code === selectedOfferingCode,
119 | )
120 | if (!selectedOffering && offerings?.length) selectedOffering = offerings[0]
121 |
122 | return (
123 |
124 |
125 |
130 |
131 |
132 | {errorMessage ? (
133 |
141 | ) : isDesktop ? (
142 |
148 | ) : (
149 |
155 | )}
156 |
157 |
158 |
159 | )
160 | }
161 |
162 | export default connector(OfferingsPage)
163 |
--------------------------------------------------------------------------------
/src/pages/PasswordResetPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useHistory } from 'react-router-dom'
3 |
4 | import Button from '@material-ui/core/Button'
5 | import CircularProgress from '@material-ui/core/CircularProgress'
6 | import Container from '@material-ui/core/Container'
7 | import Grid from '@material-ui/core/Grid'
8 | import Typography from '@material-ui/core/Typography'
9 |
10 | import api from 'API'
11 | import Navbar from 'components/Navbar'
12 | import PasswordInput from 'components/PasswordInput'
13 | import { useErrorDialog, useMySnackbar } from 'hooks'
14 | import { buildURI as buildLoginPageURI } from 'pages/LoginPage'
15 | import { validatePassword } from 'utils'
16 |
17 | export function buildURI(): string {
18 | return '/account/password_reset'
19 | }
20 |
21 | const textFieldCommonProps = {
22 | variant: 'outlined',
23 | color: 'secondary',
24 | size: 'small',
25 | fullWidth: true,
26 | }
27 |
28 | const PasswordResetPage = (): React.ReactElement => {
29 | const [password, setPassword] = useState(['', ''])
30 | const [editedPasswordField, setEditedPasswordField] = useState([
31 | false,
32 | false,
33 | ])
34 | const [pending, setPending] = useState(false)
35 |
36 | // get token from URL params
37 | const token = new URLSearchParams(window.location.search).get('token')
38 |
39 | const notify = useMySnackbar()
40 | const uspyAlert = useErrorDialog()
41 |
42 | const history = useHistory()
43 |
44 | const passwordValid = validatePassword(password[0])
45 | const passwordsMatch = password[0] === password[1]
46 |
47 | const resetPassword = () => {
48 | setPending(true)
49 | api.resetPassword(token, password[0])
50 | .then(() => {
51 | setPending(false)
52 | setTimeout(() => {
53 | history.replace(buildLoginPageURI())
54 | }, 1500)
55 | notify('Senha redefinida com sucesso!', 'success')
56 | })
57 | .catch(err => {
58 | setPending(false)
59 | if (err.code === 'bad_request') {
60 | uspyAlert('Token inválido!')
61 | } else if (err.code === 'not_found') {
62 | uspyAlert('O usuário não existe!')
63 | } else {
64 | uspyAlert(
65 | `Algo deu errado (${err.message}). Tente novamente mais tarde!`,
66 | )
67 | }
68 | })
69 | }
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
85 | {' '}
86 | A sua nova senha deve conter no mínimo 8
87 | caracteres, com pelo menos um símbolo e um
88 | número.{' '}
89 |
90 |
91 |
92 |
93 |
104 | setEditedPasswordField([
105 | true,
106 | editedPasswordField[1],
107 | ])
108 | }
109 | onChange={(evt: any) =>
110 | setPassword([evt.target.value, password[1]])
111 | }
112 | {...textFieldCommonProps}
113 | />
114 |
115 |
116 |
129 | setEditedPasswordField([
130 | editedPasswordField[1],
131 | true,
132 | ])
133 | }
134 | onChange={(evt: any) =>
135 | setPassword([password[0], evt.target.value])
136 | }
137 | {...textFieldCommonProps}
138 | />
139 |
140 |
141 |
149 | {pending ? (
150 |
154 | ) : (
155 | 'Redefinir senha'
156 | )}
157 |
158 |
159 |
160 |
161 |
162 |
163 | )
164 | }
165 |
166 | export default PasswordResetPage
167 |
--------------------------------------------------------------------------------
/src/pages/RegisterPage/style.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | height: 100%;
6 | }
7 |
8 | main {
9 | flex-grow: 1;
10 | }
11 |
12 | .figure {
13 | flex-direction: column;
14 | justify-content: center;
15 | align-items: stretch;
16 | }
17 |
18 | .center-horizontal {
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 | figcaption {
24 | text-align: center;
25 | }
--------------------------------------------------------------------------------
/src/pages/SubjectPage/CreditsIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Grid from '@material-ui/core/Grid'
4 | import { useTheme } from '@material-ui/core/styles'
5 |
6 | import Circle from 'components/Circle'
7 |
8 | interface Props {
9 | title: string
10 | value: number
11 | }
12 |
13 | const CreditsIndicator: React.FC = ({ title, value }) => {
14 | const theme = useTheme()
15 |
16 | return (
17 |
23 | {title}
24 |
25 | {' '}
26 | {value}
{' '}
27 |
28 |
29 | )
30 | }
31 |
32 | export default CreditsIndicator
33 |
--------------------------------------------------------------------------------
/src/pages/SubjectPage/GradeDistributionChart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Paper from '@material-ui/core/Paper'
4 |
5 | import {
6 | ResponsiveContainer,
7 | XAxis,
8 | YAxis,
9 | BarChart,
10 | Bar,
11 | Label,
12 | ReferenceLine,
13 | Tooltip,
14 | } from 'recharts'
15 |
16 | interface ReferenceLineLabelProps {
17 | viewBox: any
18 | width: number
19 | message: string
20 | margin?: number
21 | x: number
22 | }
23 |
24 | // Renders the flag for average and "you are here" reference lines
25 | const ReferenceLineLabel: React.FC = ({
26 | viewBox,
27 | width,
28 | message,
29 | margin = 0,
30 | x,
31 | }) => {
32 | return (
33 | <>
34 |
43 |
47 |
58 |
59 | >
60 | )
61 | }
62 |
63 | const CustomTooltip = ({ active, payload, label, total }: any) => {
64 | return active ? (
65 |
66 | Entre {(label - 1).toFixed(0) + '-' + (label + 1).toFixed(0)}:{' '}
67 | {payload ? ((100 * payload[0].value) / total).toFixed(1) : null}%
68 |
69 | ) : null
70 | }
71 |
72 | interface GradeDistributionChartProps {
73 | grades: any
74 | averageGrade: number
75 | yourGrade: number | null
76 | }
77 |
78 | const GradeDistributionChart: React.FC = ({
79 | grades,
80 | averageGrade,
81 | yourGrade,
82 | }) => {
83 | const cnt = [0, 0, 0, 0, 0] // [0, 2), [2, 4), [4, 6), [6, 8), [8, 10]
84 | for (let i = 0.0; i <= 10; i += 0.1) {
85 | if (grades[i.toFixed(1)] === undefined) {
86 | grades[i.toFixed(1)] = 0
87 | }
88 | cnt[Math.floor(i / 2) - (i === 10 ? 1 : 0)] += grades[i.toFixed(1)]
89 | }
90 | const data = cnt.reduce(
91 | (cur, val, idx) => [...cur, { x: idx * 2 + 1, grade: val }],
92 | [],
93 | )
94 | data.sort((x, y) => x.x - y.x)
95 | const total = cnt.reduce((cur, val) => cur + val, 0)
96 | const maxVal = cnt.reduce((cur, val) => Math.max(cur, val), 0)
97 |
98 | const referenceLines = [
99 | {
100 | message: 'Você está aqui',
101 | grade: yourGrade,
102 | width: 100,
103 | margin: 35,
104 | },
105 | {
106 | message: 'Média',
107 | grade: averageGrade,
108 | width: 50,
109 | margin: 0,
110 | },
111 | ]
112 |
113 | if ((yourGrade ?? 0) > averageGrade) {
114 | // whichever is greater should be rendered last, so flag is not crossed by red lines
115 | referenceLines.reverse()
116 | }
117 |
118 | return (
119 |
127 |
128 |
129 |
130 |
136 |
141 |
142 |
146 |
152 |
153 | } />
154 |
155 | {referenceLines.map((lineProps, idx) =>
156 | lineProps.grade ? (
157 |
162 | (
165 |
172 | )}
173 | />
174 |
175 | ) : null,
176 | )}
177 |
178 |
179 |
180 | )
181 | }
182 |
183 | export default GradeDistributionChart
184 |
--------------------------------------------------------------------------------
/src/pages/SubjectPage/style.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | height: 100%;
6 | }
7 |
8 | main {
9 | flex-grow: 1;
10 | height: 100%;
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/pages/TeachersPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | import MuiAccordion from '@material-ui/core/Accordion'
4 | import MuiAccordionDetails from '@material-ui/core/AccordionDetails'
5 | import MuiAccordionSummary from '@material-ui/core/AccordionSummary'
6 | import Container from '@material-ui/core/Container'
7 | import List from '@material-ui/core/List'
8 | import ListItem from '@material-ui/core/ListItem'
9 | import ListItemText from '@material-ui/core/ListItemText'
10 | import Paper from '@material-ui/core/Paper'
11 | import { withStyles } from '@material-ui/core/styles'
12 | import Typography from '@material-ui/core/Typography'
13 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
14 |
15 | import TeachersData from '__mocks__/AllProfessors'
16 | import Navbar from 'components/Navbar'
17 |
18 | interface TeacherInfo {
19 | Name: string
20 | Department: string
21 | }
22 |
23 | const Accordion = withStyles({
24 | root: {
25 | border: '1px solid rgba(0, 0, 0, .125)',
26 | boxShadow: 'none',
27 | '&:not(:last-child)': {
28 | borderBottom: 0,
29 | },
30 | '&:before': {
31 | display: 'none',
32 | },
33 | '&$expanded': {
34 | margin: 'auto',
35 | },
36 | },
37 | expanded: {},
38 | })(MuiAccordion)
39 |
40 | const AccordionSummary = withStyles({
41 | root: {
42 | backgroundColor: 'rgba(0, 0, 0, .03)',
43 | borderBottom: '1px solid rgba(0, 0, 0, .125)',
44 | marginBottom: -1,
45 | minHeight: 56,
46 | '&$expanded': {
47 | minHeight: 56,
48 | },
49 | },
50 | content: {
51 | '&$expanded': {
52 | margin: '12px 0',
53 | },
54 | },
55 | expanded: {},
56 | })(MuiAccordionSummary)
57 |
58 | const AccordionDetails = withStyles(theme => ({
59 | root: {
60 | padding: 0,
61 | },
62 | }))(MuiAccordionDetails)
63 |
64 | const MyListItem = withStyles(() => ({
65 | root: {
66 | borderBottom: '1px solid #adadad',
67 | },
68 | }))(ListItem)
69 |
70 | function renderRow(
71 | t: TeacherInfo,
72 | clickCallback: (department?: string) => void,
73 | ) {
74 | return (
75 | clickCallback(t.Department)}>
79 |
80 |
81 | )
82 | }
83 |
84 | export function buildURI(): string {
85 | // TODO fix this to include teacher code
86 | return '/professores'
87 | }
88 |
89 | const TeachersPage = () => {
90 | const clickItem = (id: string) => {
91 | console.log('Clicou no id = ', id)
92 | }
93 |
94 | const [teachers, setTeachers] = useState({})
95 |
96 | useEffect(() => {
97 | const arr: any = {}
98 | TeachersData.forEach((val: TeacherInfo) => {
99 | if (!arr[val.Department]) arr[val.Department] = []
100 | arr[val.Department].push(val)
101 | })
102 | setTeachers(arr)
103 | }, [])
104 |
105 | const [expandedAccordions, setExpandedAccordions] = useState({})
106 |
107 | const handleAccordionClick = (key: string, state: boolean) => {
108 | setExpandedAccordions({
109 | ...expandedAccordions,
110 | [key]: state,
111 | })
112 | }
113 | const accordions = Object.keys(teachers).map((key: string) => {
114 | const isExpanded = !!expandedAccordions[key]
115 | return (
116 | handleAccordionClick(key, !isExpanded)}
121 | TransitionProps={{ timeout: 200 }}>
122 | }>
123 |
124 | {key}
125 |
126 |
127 |
128 |
134 | {teachers[key].map((t: TeacherInfo) =>
135 | renderRow(t, clickItem),
136 | )}
137 |
138 |
139 |
140 | )
141 | })
142 | return (
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | {' '}
151 | Dentro de cada departamento, a lista é organizada
152 | alfabeticamente{' '}
153 |
154 |
155 | {accordions}
156 |
157 |
158 |
159 | )
160 | }
161 |
162 | export default TeachersPage
163 |
--------------------------------------------------------------------------------
/src/reducer/index.tsx:
--------------------------------------------------------------------------------
1 | import { AppState, ReduxAction, DialogError } from 'types/redux'
2 | import { User, unknownUser, guestUser } from 'types/User'
3 | // Initial state: user is not logged in
4 | const initialState: AppState = {
5 | user: unknownUser,
6 | dialogError: null,
7 | lastUpdatedAccount: '',
8 | }
9 |
10 | const reducer = (
11 | state: AppState = initialState,
12 | action: ReduxAction,
13 | ): AppState => {
14 | if (action.type === 'LOGIN') {
15 | return {
16 | ...state,
17 | user: action.payload as User,
18 | }
19 | } else if (action.type === 'LOGOUT') {
20 | return {
21 | ...state,
22 | user: guestUser,
23 | }
24 | } else if (action.type === 'ALERT') {
25 | return {
26 | ...state,
27 | dialogError: action.payload as DialogError,
28 | }
29 | } else if (action.type === 'SET_LAST_UPDATED_ACCOUNT') {
30 | return {
31 | ...state,
32 | lastUpdatedAccount: action.payload as string,
33 | }
34 | }
35 |
36 | // action is unknown, do nothing
37 | return state
38 | }
39 |
40 | export default reducer
41 |
--------------------------------------------------------------------------------
/src/routes/LoggedInRoute.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect, ConnectedProps } from 'react-redux'
3 | import { RouteProps, Route, Redirect } from 'react-router'
4 |
5 | import { AppState } from 'types/redux'
6 | import { unknownUser, guestUser } from 'types/User'
7 |
8 | const mapStateToProps = (state: AppState) => ({
9 | user: state.user,
10 | })
11 | const connector = connect(mapStateToProps)
12 |
13 | type LoggedInRouteProps = ConnectedProps & RouteProps
14 |
15 | // This redirects to the login page if the user is logged out
16 | const LoggedInRoute = ({ user, ...rest }: LoggedInRouteProps) => {
17 | if (user === unknownUser)
18 | return null // Render nothing, because what we have means that we are waiting for the account/profile request
19 | else if (user === guestUser) {
20 | return (
21 |
27 | )
28 | } else return
29 | }
30 |
31 | export default connector(LoggedInRoute)
32 |
--------------------------------------------------------------------------------
/src/routes/LoggedOutRoute.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect, ConnectedProps } from 'react-redux'
3 | import { RouteProps, Route, Redirect } from 'react-router'
4 |
5 | import { AppState } from 'types/redux'
6 | import { unknownUser, guestUser } from 'types/User'
7 |
8 | const mapStateToProps = (state: AppState) => ({
9 | user: state.user,
10 | })
11 | const connector = connect(mapStateToProps)
12 |
13 | type LoggedOutRouteProps = ConnectedProps & RouteProps
14 |
15 | // This redirects to the home page if the user is logged in
16 | const LoggedOutRoute = ({ user, ...rest }: LoggedOutRouteProps) => {
17 | if (user === unknownUser)
18 | return null // Render nothing, because what we have means that we are waiting for the account/profile request
19 | else if (user !== guestUser) {
20 | return (
21 |
26 | )
27 | } else return
28 | }
29 |
30 |
31 | export default connector(LoggedOutRoute)
32 |
--------------------------------------------------------------------------------
/src/routes/WithMetaRoute.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from 'react-helmet'
3 | import { RouteProps, Route } from 'react-router'
4 | /*
5 | This is a wrapper of a route with an additional meta description
6 | */
7 |
8 | interface WithMetaRoutePropsType extends RouteProps {
9 | title?: string
10 | description?: string
11 | robots?: string[]
12 | element?: React.ComponentType
13 | }
14 |
15 | const WithMetaRoute: React.FC = (
16 | props: WithMetaRoutePropsType,
17 | ) => {
18 | const {
19 | title,
20 | element: ChildrenElement,
21 | description,
22 | robots,
23 | ...rest
24 | } = props
25 | return (
26 |
27 |
28 | {title ? {title} : null}
29 | {description ? (
30 |
31 | ) : null}
32 | {robots ? (
33 |
34 | ) : null}
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default WithMetaRoute
42 |
--------------------------------------------------------------------------------
/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from '@material-ui/core/styles'
2 |
3 | // To find out light and dark variations of colors: https://material-ui.com/pt/customization/color/#picking-colors
4 | const primaryColor = '#68417f'
5 | const secondaryColor = '#415c77'
6 | const theme = createTheme({
7 | palette: {
8 | primary: {
9 | light: '#A77BC3',
10 | main: primaryColor,
11 | dark: '#482d58',
12 | contrastText: '#fff',
13 | },
14 | secondary: {
15 | light: '#677c92',
16 | main: secondaryColor,
17 | dark: '#2d4053',
18 | contrastText: '#000',
19 | },
20 | },
21 | overrides: {
22 | MuiCardHeader: {
23 | action: {
24 | marginTop: 0,
25 | marginRight: 0,
26 | },
27 | },
28 | MuiCollapse: {
29 | /* Used for notistack-snackbar centering on mobile. */
30 | wrapper: {
31 | justifyContent: 'center',
32 | alignItems: 'center',
33 | },
34 | },
35 | MuiContainer: {
36 | root: {
37 | paddingLeft: 8,
38 | paddingRight: 8,
39 | },
40 | },
41 | MuiTableCell: {
42 | head: {
43 | fontSize: '1.125rem',
44 | },
45 | root: {
46 | fontSize: '1rem',
47 | },
48 | },
49 | MuiDialog: {
50 | paper: {
51 | margin: 16,
52 | },
53 | },
54 | // The following line is a workaround for https://github.com/mui-org/material-ui/issues/26251
55 | MuiButton: {
56 | root: {
57 | transition: 'color .01s',
58 | },
59 | },
60 | },
61 | })
62 |
63 | export default theme
64 |
--------------------------------------------------------------------------------
/src/types/Auth.ts:
--------------------------------------------------------------------------------
1 | export type Auth = string
2 |
--------------------------------------------------------------------------------
/src/types/Course.ts:
--------------------------------------------------------------------------------
1 | export interface Institute {
2 | name: string
3 | code: string
4 | }
5 |
6 | export interface Course {
7 | name: string
8 | code: string
9 | specialization: string
10 | }
11 |
12 | export interface CourseComplete extends Course {
13 | shift: string
14 | subjects: { [code: string]: string }
15 | }
16 |
--------------------------------------------------------------------------------
/src/types/Offering.ts:
--------------------------------------------------------------------------------
1 | export interface Offering {
2 | professor: string
3 | code: string
4 | years: string[]
5 | approval: number
6 | neutral: number
7 | disapproval: number
8 | }
9 |
10 | export interface OfferingInfo {
11 | professor: string
12 | code: string
13 | }
14 |
15 | export interface OfferingReview {
16 | uuid: string
17 | rating: number
18 | body: string
19 | edited: boolean
20 | timestamp: string
21 | upvotes: number
22 | downvotes: number
23 | verified?: boolean
24 | }
25 |
26 | export interface OfferingReviewVote {
27 | type: 'upvote' | 'downvote' | 'none'
28 | }
29 |
--------------------------------------------------------------------------------
/src/types/Record.ts:
--------------------------------------------------------------------------------
1 | export interface Record {
2 | name: string
3 | code: string
4 | grade: number
5 | frequency: number
6 | status: string
7 | completed?: boolean
8 | course?: string
9 | specialization?: string
10 | reviewed?: boolean
11 | }
12 |
--------------------------------------------------------------------------------
/src/types/Stats.ts:
--------------------------------------------------------------------------------
1 | export interface Stats {
2 | comments: number
3 | grades: number
4 | offerings: number
5 | subjects: number
6 | users: number
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/Subject.ts:
--------------------------------------------------------------------------------
1 | export interface SubjectInfo {
2 | code: string
3 | name: string
4 | }
5 |
6 | export interface SubjectKey {
7 | course: string
8 | code: string
9 | specialization: string
10 | name?: string
11 | }
12 |
13 | export interface SubjectRequirement extends SubjectInfo {
14 | strong: boolean
15 | }
16 |
17 | export interface Subject {
18 | name: string
19 | code: string
20 | course: string
21 | specialization: string
22 | description: string
23 | class: number
24 | assign: number
25 | hours: string
26 | requirements: SubjectRequirement[][]
27 | optional: boolean
28 | semester: number
29 | stats: {
30 | total: number
31 | worth_it: number
32 | }
33 | }
34 |
35 | export interface SubjectSibling {
36 | code: string
37 | name: string
38 | optional: boolean
39 | }
40 |
41 | export interface SubjectRelations {
42 | code: string
43 | predecessors: SubjectRequirement[][]
44 | successors: SubjectRequirement[]
45 | }
46 |
47 | export interface SubjectReview {
48 | categories: {
49 | worth_it: boolean
50 | }
51 | }
52 |
53 | export interface SubjectGradeStats {
54 | grades: any
55 | average: number
56 | approval: number
57 | }
58 |
59 | export interface SubjectGrade {
60 | grade: number
61 | status: string
62 | frequency: number
63 | }
64 |
--------------------------------------------------------------------------------
/src/types/User.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | user: string
3 | name: string
4 | }
5 | export const guestUser: User = {
6 | user: '0',
7 | name: '',
8 | }
9 |
10 | export const unknownUser: User = {
11 | user: '',
12 | name: '',
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | export default any
3 | }
4 |
5 | declare module '*.svg?react' {
6 | export default React.FunctionComponent>
7 | }
8 |
9 | declare module '*.png' {
10 | export default string
11 | }
12 |
13 | declare module '*.gif' {
14 | export default string
15 | }
16 |
17 | declare module '*.jpg' {
18 | export default string
19 | }
20 |
21 | declare module '*.jpeg' {
22 | export default string
23 | }
24 |
--------------------------------------------------------------------------------
/src/types/redux.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'redux'
2 |
3 | import { User } from 'types/User'
4 |
5 | export interface DialogError {
6 | message: string
7 | title?: string
8 | }
9 |
10 | export interface AppState {
11 | user: User
12 | dialogError: DialogError | null
13 | lastUpdatedAccount: string
14 | }
15 |
16 | // Types of Redux Actions
17 | export type ReduxActionType =
18 | | 'LOGIN'
19 | | 'LOGOUT'
20 | | 'ALERT'
21 | | 'SET_LAST_UPDATED_ACCOUNT'
22 |
23 | export interface ReduxAction extends Action {
24 | type: ReduxActionType
25 | payload:
26 | | User // 'LOGIN'
27 | | null // 'LOGOUT'
28 | | DialogError // 'ALERT'
29 | | string // 'SET_LAST_UPDATED_ACCOUNT'
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function copyObj(obj: any) {
2 | return JSON.parse(JSON.stringify(obj))
3 | }
4 |
5 | function isDigit(c: string) {
6 | return c >= '0' && c <= '9'
7 | }
8 |
9 | function isSpecialCharacter(c: string) {
10 | return ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.includes(c)
11 | }
12 |
13 | // Returns true if password has at least 8 digits with at least one number and one special character
14 | export function validatePassword(pwd: string) {
15 | let hasDigit = false
16 | let hasSpecialCharacter = false
17 | for (let i = 0; i < pwd.length; ++i) {
18 | hasDigit = hasDigit || isDigit(pwd[i])
19 | hasSpecialCharacter = hasSpecialCharacter || isSpecialCharacter(pwd[i])
20 | }
21 |
22 | return hasDigit && hasSpecialCharacter && pwd.length >= 8
23 | }
24 |
25 | // Returns the capitalized initials of a given string with multiple words
26 | export function getInitials(name: string): string {
27 | try {
28 | const words = name.split(/\s+/)
29 | return words.reduce((prev, cur) => {
30 | const caps = cur[0].toUpperCase()
31 | if (cur[0] === caps && caps >= 'A' && caps <= 'Z') {
32 | return prev + cur[0]
33 | }
34 |
35 | return prev
36 | }, '')
37 | } catch {
38 | return ''
39 | }
40 | }
41 |
42 | // Returns true if email is valid and has domain "usp.br"
43 | export function validateEmail(email: string) {
44 | const reg =
45 | /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$/
46 | const res = reg.exec(email)
47 | return res && res[1] === 'usp.br'
48 | }
49 |
50 | export function unique(arr: T[]): T[] {
51 | const ret: T[] = []
52 |
53 | const st = new Set()
54 | arr.forEach(el => {
55 | if (!st.has(el)) {
56 | ret.push(el)
57 | st.add(el)
58 | }
59 | })
60 | return ret
61 | }
62 |
--------------------------------------------------------------------------------
/src/utils/time.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Levels:
3 | < 5m - Agora
4 | < 60m - Xm atrás
5 | < 24h - Xh atrás
6 | < 7 dias - às hh::mm
7 | DD/MM/YY às hh:mm
8 |
9 | */
10 |
11 | const WEEK_DAY = [
12 | 'Domingo',
13 | 'Segunda',
14 | 'Terça',
15 | 'Quarta',
16 | 'Quinta',
17 | 'Sexta',
18 | 'Sábado',
19 | ]
20 |
21 | export function getRelativeDate(d: Date): string {
22 | const diff = new Date().getTime() - d.getTime()
23 | if (diff < 5 * 60 * 1000) {
24 | return 'Agora'
25 | } else if (diff < 60 * 60 * 1000) {
26 | const minutes = Math.round(diff / (60 * 1000)).toFixed(0)
27 | return `${minutes}m atrás`
28 | } else if (diff < 24 * 60 * 60 * 1000) {
29 | const hours = Math.round(diff / (60 * 60 * 1000)).toFixed(0)
30 | return `${hours}h atrás`
31 | } else if (diff < 7 * 24 * 60 * 60 * 1000) {
32 | return `${WEEK_DAY[d.getDay()]} às ${getHours(d)}:${getMinutes(d)}`
33 | } else {
34 | return `${d.toLocaleDateString()} às ${getHours(d)}:${getMinutes(d)}`
35 | }
36 | }
37 |
38 | export function toSlashSeparatedDate(d: Date): string {
39 | return d.toLocaleDateString('pt-BR')
40 | }
41 |
42 | export function getHours(d: Date): string {
43 | return d.getHours().toLocaleString('pt-BR', { minimumIntegerDigits: 2 })
44 | }
45 |
46 | export function getMinutes(d: Date): string {
47 | return d.getMinutes().toLocaleString('pt-BR', { minimumIntegerDigits: 2 })
48 | }
49 |
50 | export function toTime(d: Date): string {
51 | const hours = getHours(d)
52 | const minutes = getMinutes(d)
53 | return hours + ':' + minutes
54 | }
55 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv')
2 | const path = require('path')
3 | const webpack = require('webpack')
4 |
5 | const HtmlWebpackPlugin = require('./html-webpack-plugin.js')
6 | const ESLintPlugin = require('eslint-webpack-plugin')
7 |
8 | // Options for development mode
9 | const devOptions = {
10 | watchOptions: {
11 | poll: 1000, // polls every second
12 | },
13 | devtool: 'inline-source-map',
14 | devServer: {
15 | historyApiFallback: true,
16 | },
17 | }
18 |
19 | function buildConfig(env, argv) {
20 | let envVars = {
21 | API_URL:
22 | argv.mode === 'development'
23 | ? 'https://dev.uspy.me'
24 | : 'https://prod.uspy.me',
25 | }
26 | if (env.local) {
27 | envVars = Object.assign(
28 | {
29 | API_URL: 'http://127.0.0.1:8080',
30 | },
31 | dotenv.config({
32 | path: path.join(__dirname, '.env'),
33 | }).parsed,
34 | )
35 | }
36 | const envKeys = Object.keys(envVars).reduce((prev, next) => {
37 | prev[`process.env.${next}`] = JSON.stringify(envVars[next])
38 | return prev
39 | }, {})
40 |
41 | return Object.assign(
42 | {
43 | entry: path.join(__dirname, 'src', 'index'),
44 | output: {
45 | filename: 'bundle.js',
46 | path: path.join(__dirname, 'build', 'static'),
47 | publicPath: '/static/',
48 | },
49 | module: {
50 | rules: [
51 | {
52 | test: /\.(ts|js)x?$/,
53 | exclude: /node_modules/,
54 | include: /src/,
55 | use: ['babel-loader', 'ts-loader'],
56 | },
57 | {
58 | test: /\.css$/,
59 | use: ['style-loader', 'css-loader'],
60 | },
61 | {
62 | test: /\.(png|jpg|jpeg|gif|svg)$/,
63 | resourceQuery: { not: [/react/] },
64 | type: 'asset/resource',
65 | },
66 | {
67 | test: /\.svg$/,
68 | resourceQuery: /react/,
69 | use: ['@svgr/webpack'],
70 | }
71 | ],
72 | },
73 | resolve: {
74 | extensions: ['.tsx', '.ts', '.js'],
75 | modules: [
76 | path.join(__dirname, 'node_modules'),
77 | path.join(__dirname, 'src'),
78 | ],
79 | },
80 | plugins: [
81 | new webpack.DefinePlugin(envKeys),
82 | new HtmlWebpackPlugin({
83 | favicon: './favicon.ico',
84 | }),
85 | new ESLintPlugin({})
86 | ],
87 | },
88 | env.local ? devOptions : {},
89 | )
90 | }
91 |
92 | module.exports = buildConfig
93 |
--------------------------------------------------------------------------------