├── .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 | 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 | 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 |