├── .github ├── ISSUE_TEMPLATE │ └── issue_template.md ├── images │ └── run_and_debug.png ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── launch.json ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── backend ├── .env.example ├── .golangci.yml ├── Makefile ├── cmd │ ├── api │ │ └── main.go │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ └── migrate │ │ └── main.go ├── config │ └── config.go ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── client.go │ │ └── token.go │ ├── database │ │ ├── database.go │ │ └── seed.go │ ├── models │ │ ├── events.go │ │ ├── student.go │ │ └── token.go │ ├── repository │ │ ├── event_repository.go │ │ ├── student_repository.go │ │ └── token_repository.go │ └── services │ │ ├── calendar_service.go │ │ ├── email_service.go │ │ ├── event_service.go │ │ ├── gmail_service.go │ │ ├── google_calendar_service.go │ │ ├── linkedin_service.go │ │ ├── student_service.go │ │ └── token_service.go ├── makefile ├── pkg │ ├── http │ │ └── http.go │ └── utils │ │ └── encoding.go ├── static │ └── imgs │ │ ├── .DS_Store │ │ ├── faladev.ico │ │ ├── faladev.jpg │ │ ├── instagram.svg │ │ ├── whatsapp.svg │ │ └── youtube.svg ├── templates │ ├── email │ │ └── mentorship.html │ └── web │ │ └── form.html ├── tests │ ├── google_calendar_service_mocks.go │ ├── google_calendar_service_test.go │ └── linkedIn_service_test.go └── tools │ └── Makefile ├── docker-compose.yml ├── entrypoint.sh ├── frontend ├── .eslintrc.json ├── .gitignore ├── .hintrc ├── .nvmrc ├── .pre-commit-config.yaml ├── .prettierrc ├── .storybook │ ├── main.ts │ └── preview.tsx ├── .vscode │ └── launch.json ├── README.md ├── auto-imports.d.ts ├── components.json ├── cypress.config.ts ├── cypress │ ├── e2e │ │ ├── Mentoring │ │ │ ├── MentotingPage.ts │ │ │ └── mentoring.spec.cy.ts │ │ └── coverage │ │ │ ├── base.css │ │ │ ├── block-navigation.js │ │ │ ├── coverage-final.json │ │ │ ├── favicon.png │ │ │ ├── index.html │ │ │ ├── prettify.css │ │ │ ├── prettify.js │ │ │ ├── sort-arrow-sprite.png │ │ │ └── sorter.js │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── commands.ts │ │ └── e2e.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── assets │ │ └── person-unknow.png │ ├── next.svg │ ├── static │ │ └── imgs │ │ │ ├── faladev.ico │ │ │ ├── faladev.jpg │ │ │ ├── instagram.svg │ │ │ ├── whatsapp.svg │ │ │ └── youtube.svg │ └── vercel.svg ├── src │ ├── Mutate │ │ ├── useMutationMentoring.ts │ │ └── useUserMutation.ts │ ├── Provider │ │ └── ReactQueryProvider.tsx │ ├── app │ │ ├── (mentoring) │ │ │ ├── mentoring.model.spec.tsx │ │ │ ├── mentoring.model.ts │ │ │ ├── mentoring.schema.ts │ │ │ ├── mentoring.type.ts │ │ │ ├── mentoring.view.tsx │ │ │ ├── page.spec.tsx │ │ │ └── page.tsx │ │ ├── (users) │ │ │ └── register │ │ │ │ ├── page.tsx │ │ │ │ ├── register.model.spec.ts │ │ │ │ ├── register.view.tsx │ │ │ │ ├── types.ts │ │ │ │ ├── user.model.ts │ │ │ │ └── user.schema.ts │ │ ├── globals.css │ │ ├── layout.spec.tsx │ │ └── layout.tsx │ ├── components │ │ ├── Buttons │ │ │ └── ButtonWhiteBlack │ │ │ │ └── index.tsx │ │ ├── form │ │ │ └── text-input.tsx │ │ ├── ui │ │ │ ├── alert-box │ │ │ │ ├── alert-boc.spec.tsx │ │ │ │ ├── alert-box.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── alert │ │ │ │ ├── alert.spec.tsx │ │ │ │ ├── alert.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── button │ │ │ │ ├── button.spec.tsx │ │ │ │ ├── button.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── card │ │ │ │ ├── card.spec.tsx │ │ │ │ ├── card.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── error-message │ │ │ │ ├── error-message.spec.tsx │ │ │ │ ├── error-message.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── input │ │ │ │ ├── Input.stories.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── input.spec.tsx │ │ │ └── label │ │ │ │ ├── index.tsx │ │ │ │ ├── label.spec.tsx │ │ │ │ └── label.stories.tsx │ │ └── upload │ │ │ ├── MockFormProvider.tsx │ │ │ ├── index.tsx │ │ │ ├── upload.spec.tsx │ │ │ └── upload.stories.tsx │ ├── infra │ │ └── http │ │ │ ├── HttpClient.spec.ts │ │ │ ├── HttpClient.ts │ │ │ └── HttpClient.types.ts │ ├── lib │ │ ├── utils.spec.ts │ │ └── utils.ts │ ├── services │ │ ├── MentoringAgenda │ │ │ ├── MentoringAgenda.service.spec.ts │ │ │ └── MentoringAgenda.service.ts │ │ └── User │ │ │ └── User.service.ts │ ├── shared │ │ └── registrationStatusMessages.ts │ ├── test-utils │ │ └── index.tsx │ └── tests │ │ ├── changeInput.tsx │ │ ├── mock │ │ ├── httpClientMock.ts │ │ ├── mentoringServiceMock.ts │ │ ├── mockSchemaMentoringTypeData.ts │ │ └── userServiceMock.ts │ │ ├── renderView.tsx │ │ ├── renderWithQueryClient.tsx │ │ └── withReactQueryProvider.tsx ├── tailwind.config.ts ├── tsconfig.json ├── vitest.config.ts ├── vitest.setup.mts └── yarn.lock ├── servers.json └── yarn.lock /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Descrição 2 | 3 | Esta issue propõe a implementação de Docker Compose no projeto para facilitar a configuração e orquestração de múltiplos serviços, incluindo PostgreSQL, pgAdmin, Jaeger e a aplicação principal. A proposta é criar um ambiente de desenvolvimento mais eficiente e simplificado. 4 | 5 | ## Objetivo 6 | 7 | Adicionar a configuração de Docker Compose para gerenciar os seguintes serviços: 8 | 9 | - PostgreSQL: Banco de dados relacional 10 | - pgAdmin: Interface gráfica para gerenciamento do PostgreSQL 11 | - Jaeger: Ferramenta de rastreamento e monitoramento 12 | - Aplicação principal: Serviço que inclui as funcionalidades do projeto 13 | 14 | ## Justificativa 15 | 16 | A configuração manual de múltiplos serviços e dependências pode ser trabalhosa e propensa a erros. Usando Docker Compose, podemos definir e gerenciar todos os serviços necessários em um único arquivo, simplificando o processo de desenvolvimento e testes. Isso melhora a eficiência do desenvolvimento e garante um ambiente consistente. -------------------------------------------------------------------------------- /.github/images/run_and_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/.github/images/run_and_debug.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Descrição 2 | 3 | Este pull request introduz melhorias no template HTML da página de erro. A principal melhoria é o aumento do tamanho da fonte do código de erro e da mensagem para torná-los mais destacados e fáceis de ler. 4 | 5 | ## Mudanças Realizadas 6 | 7 | 1. **Aumento do Tamanho da Fonte**: 8 | - Adicionadas estilos CSS personalizados para aumentar o tamanho da fonte do código de erro e da mensagem. 9 | - Aplicadas classes utilitárias do Tailwind CSS para garantir que o texto esteja centralizado. 10 | 11 | ### Arquivos Modificados 12 | 13 | - `templates/web/error.html`: Estrutura HTML atualizada e adição de estilos personalizados. 14 | 15 | ### Mudanças Detalhadas 16 | 17 | - **Tamanho da Fonte do Código de Erro**: 18 | - Adicionada a classe CSS `.error-code` com `font-size: 4rem;` para aumentar o tamanho da fonte do código de erro. 19 | - **Tamanho da Fonte da Mensagem de Erro**: 20 | - Adicionada a classe CSS `.error-message` com `font-size: 1.5rem;` para aumentar o tamanho da fonte da mensagem de erro. 21 | - **Alinhamento Central**: 22 | - Aplicada a classe `text-center` do Tailwind CSS para centralizar o texto dentro do contêiner. 23 | 24 | ## Antes e Depois 25 | 26 | ### Antes 27 | 28 | ![Screenshot Antes](link_para_screenshot_antes) 29 | 30 | ### Depois 31 | 32 | ![Screenshot Depois](link_para_screenshot_depois) 33 | 34 | ## Motivação e Contexto 35 | 36 | O tamanho da fonte anterior para o código de erro e a mensagem era muito pequeno, dificultando para os usuários identificarem e entenderem rapidamente o erro. Aumentar o tamanho da fonte melhora a legibilidade e a experiência do usuário. 37 | 38 | ## Como Isso Foi Testado? 39 | 40 | - Testado manualmente a página de erro para garantir que os novos tamanhos de fonte sejam aplicados corretamente e o texto permaneça centralizado. 41 | 42 | ## Issue Relacionada 43 | 44 | - [Issue #123](link_para_issue_relacionada) 45 | 46 | ## Tipos de Mudanças 47 | 48 | - [ ] Correção de bug (mudança que não quebra a compatibilidade e corrige um problema) 49 | - [x] Nova funcionalidade (mudança que não quebra a compatibilidade e adiciona uma funcionalidade) 50 | - [ ] Mudança que quebra a compatibilidade (correção ou funcionalidade que causa uma mudança em funcionalidades existentes) 51 | 52 | ## Checklist 53 | 54 | - [x] Meu código segue o estilo de código deste projeto. 55 | - [x] Minha mudança requer uma mudança na documentação. 56 | - [x] Eu atualizei a documentação conforme necessário. 57 | - [ ] Eu adicionei testes para cobrir minhas mudanças. 58 | - [x] Todos os novos e antigos testes passaram. 59 | 60 | ## Notas Adicionais 61 | 62 | - Qualquer informação ou contexto adicional que os revisores possam precisar saber. 63 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '18.17.0' 19 | 20 | - name: Install frontend dependencies 21 | working-directory: ./frontend 22 | run: npm install 23 | 24 | - name: Build frontend 25 | working-directory: ./frontend 26 | run: npm run build 27 | 28 | #- name: Test frontend 29 | # working-directory: ./frontend 30 | # run: npm test -- --watchAll=false 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v4 34 | with: 35 | go-version: '1.22' 36 | 37 | - name: Install backend dependencies 38 | working-directory: ./backend 39 | run: go mod download 40 | 41 | - name: Build backend 42 | working-directory: ./backend 43 | run: go build -v ./... 44 | 45 | - name: Test backend 46 | working-directory: ./backend 47 | run: go test -v ./... 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Connect to server", 9 | "type": "go", 10 | "request": "attach", 11 | "mode": "remote", 12 | "port": 2345, 13 | "host": "127.0.0.1" 14 | }, 15 | { 16 | "name": "Attach to Process", 17 | "type": "go", 18 | "request": "attach", 19 | "mode": "local", 20 | "processId": 0 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribuindo para o projeto 2 | 3 | Agradecemos o seu interesse em contribuir para a nossa plataforma! Este documento contém as diretrizes para contribuir com o projeto. Siga-as para garantir a consistência e qualidade das contribuições. 4 | 5 | ## Pré-requisitos para Contribuir 6 | 7 | Antes de começar a contribuir, certifique-se de ter: 8 | 9 | - Docker instalado em seu ambiente. 10 | - Uma conta no Google Console para criar credenciais de API necessárias para acessar o Google Calendar e Gmail. 11 | 12 | ## Como Contribuir 13 | 14 | 1. **Fork e Clone:** 15 | 16 | Faça um fork do repositório no GitHub. 17 | Clone o seu fork para o seu ambiente de desenvolvimento local. 18 | 19 | ```bash 20 | git clone && cd faladev 21 | ``` 22 | 23 | 2. **Configurar Ambiente:** 24 | 25 | Configure as variáveis de ambiente conforme necessário para acessar serviços do Google. 26 | 27 | Execute o Docker Compose para subir a aplicação: 28 | 29 | ```bash 30 | docker-compose up -d 31 | ``` 32 | 33 | ## 3. Trabalhando com Branches 34 | 35 | Crie uma branch a partir da `main` para cada nova funcionalidade, correção ou alteração na documentação. Siga um padrão claro para nomear as branches, facilitando o entendimento e a organização do trabalho: 36 | 37 | - **feature/**: Para novas funcionalidades. 38 | 39 | - Exemplo: `feature/add-user-login` 40 | 41 | - **bugfix/**: Para correções de bugs. 42 | 43 | - Exemplo: `bugfix/fix-login-error` 44 | 45 | - **docs/**: Para mudanças na documentação. 46 | 47 | - Exemplo: `docs/add-contributing-md` 48 | 49 | - **chore/**: Para tarefas administrativas ou de manutenção. 50 | 51 | - Exemplo: `chore/update-dependencies` 52 | 53 | Crie a branch usando o comando abaixo: 54 | 55 | ```bash 56 | git checkout -b feature/nome-da-branch 57 | ``` 58 | 59 | 4. **Desenvolvimento:** 60 | 61 | - Siga as boas práticas de desenvolvimento conforme discutido no projeto. 62 | - Adicione ou atualize os testes conforme necessário. 63 | - Verifique se o código segue os padrões estabelecidos e não introduz problemas novos. 64 | 65 | 5. **Documentação:** 66 | 67 | - Atualize a documentação conforme necessário. 68 | - Se você adicionou novas funcionalidades, atualize a documentação Swagger conforme as instruções no `README.md`. 69 | 70 | 6. **Commit e Push:** 71 | 72 | Use mensagens de commit claras e descritivas. Um exemplo recomendado é seguir o padrão **Conventional Commits**, adicionando prefixos como `feat:`, `fix:`, `docs:`, e `chore:` na mensagem de commit. 73 | 74 | Envie suas alterações para o seu fork. 75 | ```bash 76 | git commit -m "Descrição clara e concisa do que foi feito" 77 | ``` 78 | 79 | ```bash 80 | git push origin nome-da-branch 81 | ``` 82 | 83 | 7. **Pull Request:** 84 | 85 | - Faça um pull request da sua branch no seu fork para a branch `main` do repositório original. 86 | - Descreva claramente o que o seu código faz e por que a sua contribuição é importante. 87 | - Link qualquer issue relevante no seu pull request. 88 | 89 | 8. **Revisão:** 90 | 91 | - Aguarde feedback ou aprovação dos mantenedores do projeto. 92 | - Faça as alterações necessárias se solicitado pelos revisores. 93 | 94 | ## Código de Conduta 95 | 96 | Ao participar deste projeto, espera-se que você trate todos os contribuidores com respeito e contribua ativamente para a criação de um ambiente acolhedor para todos, independentemente de sua senioridade. 97 | 98 | ## Dúvidas? 99 | 100 | Se tiver dúvidas ou precisar de ajuda, não hesite em abrir uma issue no GitHub para solicitar mais informações ou suporte. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine 2 | 3 | RUN go install github.com/cespare/reflex@latest 4 | RUN go install github.com/go-delve/delve/cmd/dlv@latest 5 | 6 | WORKDIR /app 7 | 8 | COPY ./backend/go.mod ./backend/go.sum ./ 9 | RUN go mod download 10 | COPY ./backend . 11 | 12 | COPY entrypoint.sh . 13 | 14 | RUN chmod +x entrypoint.sh && rm -rf /var/cache/apk/* 15 | 16 | ENTRYPOINT ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | dockerUp: 3 | @docker-compose up -d 4 | 5 | dockerDown: 6 | @docker-compose down --rmi all 7 | 8 | dockerPrune: 9 | @docker system prune -a --volumes 10 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/api -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plataforma para gestão de mentorias 2 | 3 | Este projeto é uma aplicação open source feita em Go no backend e React no frontend, inicialmente integrada aos serviços do Google Calendar e Gmail, base para discutirmos boas práticas, conceitos e fundamentos. 4 | 5 | ## Pré-requisitos 6 | 7 | - Conta no Google (necessária para criação do aplicativo que iremos utilizar para integração com Google Calendar e Gmail) 8 | - Docker instalado 9 | 10 | ## Instalação 11 | 12 | 1. **Clone o Repositório:** 13 | 14 | ```bash 15 | git clone https://github.com/dedevpradev/faladev.git 16 | cd faladev 17 | ``` 18 | 19 | 2. **Configurar Credenciais do Google Console:** 20 | 21 | Para que a aplicação possa acessar o Google Calendar e enviar emails, você precisa configurar as credenciais no Google Console: 22 | 23 | Acesse o Google Cloud Console: [Google Cloud Console](https://console.cloud.google.com/) 24 | 25 | Crie um Novo Projeto: 26 | 27 | - Vá para o painel do Google Cloud Console. 28 | - Clique em "Select a Project" e depois em "New Project". 29 | - Dê um nome ao seu projeto e clique em "Create". 30 | 31 | Habilite as APIs Necessárias: 32 | 33 | - Vá para "API & Services" > "Library". 34 | - Pesquise e habilite a API do Google Calendar. 35 | - Pesquise e habilite a API do Gmail. 36 | 37 | Configure a Tela de Consentimento OAuth: 38 | 39 | - Vá para "API & Services" > "OAuth consent screen". 40 | - Escolha "External" e clique em "Create". 41 | - Preencha as informações necessárias, como nome do aplicativo e email de suporte. 42 | - Adicione o escopo .../auth/calendar para acesso ao Google Calendar e .../auth/gmail.send para enviar emails. 43 | - Salve as alterações. 44 | 45 | Crie Credenciais OAuth 2.0: 46 | 47 | - Vá para "API & Services" > "Credentials". 48 | - Clique em "Create Credentials" e selecione "ID do cliente OAuth". 49 | - Quando solicitado, adicione os URIs de redirecionamento autorizados. Exemplo: http://localhost:8080/callback 50 | - Salve as credenciais e anote o Client ID e Client Secret. 51 | 52 | 4. **Configurar Variáveis de Ambiente:** 53 | 54 | Crie um arquivo .env no diretório ./backend e adicione as seguintes variáveis, incluindo suas credenciais do Google: 55 | 56 | ```env 57 | GOOGLE_REDIRECT_URL=http://localhost:8080/callback 58 | GOOGLE_CLIENT_ID=seu-client-id 59 | GOOGLE_CLIENT_SECRET=seu-client-secret 60 | ``` 61 | 62 | ## Instruções para executar a aplicação utilizando Docker Compose 63 | 64 | Para iniciar a aplicação, execute o comando: 65 | 66 | ```bash 67 | docker-compose up -d 68 | ``` 69 | 70 | Para parar e remover contêineres, redes, volumes e imagens usadas pelo docker compose, execute o comando: 71 | 72 | ```bash 73 | docker-compose down --rmi all 74 | ``` 75 | 76 | Para limpar caches e configurações locais, você pode remover os arquivos de configuração e imagens desnecessárias: 77 | 78 | ```bash 79 | docker system prune -a --volumes 80 | ``` 81 | 82 | ## Como Usar 83 | 84 | Para acessar a aplicação: 85 | 86 | ```bash 87 | http://localhost:3000 88 | ``` 89 | 90 | URL da API: 91 | 92 | ```bash 93 | http://localhost:8080 94 | ``` 95 | 96 | Para acessar o Jaeger: 97 | 98 | ```bash 99 | http://localhost:16686/ 100 | ``` 101 | 102 | Para acessar o pgAdmin: 103 | 104 | ```bash 105 | http://localhost:5050/ 106 | ``` 107 | 108 | Para acessar a documentação swagger: 109 | 110 | ```bash 111 | http://localhost:8080/swagger/index.html 112 | ``` 113 | 114 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | GOOGLE_MEET_EVENT= 2 | GOOGLE_REDIRECT_URL= 3 | GOOGLE_CLIENT_ID= 4 | GOOGLE_CLIENT_SECRET= 5 | 6 | LINKEDIN_CLIENT_ID="78ayhj9r8gh3ce" 7 | LINKEDIN_CLIENT_SECRET= "Tp7E0Gao0sf34NwY" 8 | LINKEDIN_REDIRECT_URL= "http://localhost:8080/callback" 9 | -------------------------------------------------------------------------------- /backend/.golangci.yml: -------------------------------------------------------------------------------- 1 | # Options for analysis running. 2 | run: 3 | # The default concurrency value is the number of available CPU. 4 | concurrency: 4 5 | # Timeout for analysis, e.g. 30s, 5m. 6 | # Default: 1m 7 | timeout: 5m 8 | # Exit code when at least one issue was found. 9 | # Default: 1 10 | issues-exit-code: 2 11 | # Include test files or not. 12 | # Default: true 13 | tests: false 14 | # List of build tags, all linters use it. 15 | # Default: []. 16 | build-tags: 17 | - mytag 18 | modules-download-mode: readonly 19 | # Allow multiple parallel golangci-lint instances running. 20 | # If false (default) - golangci-lint acquires file lock on start. 21 | allow-parallel-runners: false 22 | # Define the Go version limit. 23 | # Mainly related to generics support since go1.18. 24 | # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18 25 | go: '1.23' 26 | issues: 27 | exclude-files: 28 | exclude-dirs: 29 | exclude-dirs-use-default: false 30 | linters: 31 | # Enable specific linter 32 | # https://golangci-lint.run/usage/linters/#enabled-by-default 33 | enable: 34 | - asasalint 35 | - asciicheck 36 | - bidichk 37 | - bodyclose 38 | - containedctx 39 | # - contextcheck 40 | - cyclop 41 | - decorder 42 | # - depguard 43 | - dogsled 44 | - dupl 45 | - dupword 46 | - durationcheck 47 | - errcheck 48 | - errchkjson 49 | - errname 50 | - errorlint 51 | - exhaustive 52 | # - exhaustruct 53 | - forbidigo 54 | - forcetypeassert 55 | - funlen 56 | # - gci 57 | - ginkgolinter 58 | - gocheckcompilerdirectives 59 | # - gochecknoglobals 60 | - gochecknoinits 61 | - gocognit 62 | - goconst 63 | - gocritic 64 | - gocyclo 65 | # - godot 66 | # - godox 67 | - gofmt 68 | - gofumpt 69 | - goheader 70 | - goimports 71 | - gomoddirectives 72 | - gomodguard 73 | - goprintffuncname 74 | # - gosec 75 | - gosimple 76 | # - gosmopolitan 77 | - govet 78 | - grouper 79 | - importas 80 | - ineffassign 81 | - interfacebloat 82 | # - interfacer 83 | - ireturn 84 | - lll 85 | - loggercheck 86 | - maintidx 87 | - makezero 88 | - mirror 89 | # - misspell 90 | - musttag 91 | - nakedret 92 | # - nestif 93 | - nilerr 94 | - nilnil 95 | - nlreturn 96 | - noctx 97 | - nolintlint 98 | - nonamedreturns 99 | - nosprintfhostport 100 | - paralleltest 101 | - prealloc 102 | - predeclared 103 | - promlinter 104 | - reassign 105 | - revive 106 | - rowserrcheck 107 | - sqlclosecheck 108 | - staticcheck 109 | - stylecheck 110 | # - tagalign 111 | # - tagliatelle 112 | - tenv 113 | - testableexamples 114 | - testpackage 115 | - thelper 116 | - tparallel 117 | - typecheck 118 | - unconvert 119 | - unparam 120 | - unused 121 | - usestdlibvars 122 | # - varnamelen 123 | - wastedassign 124 | - whitespace 125 | - wrapcheck 126 | - wsl 127 | - zerologlint 128 | # Disable specific linter 129 | # https://golangci-lint.run/usage/linters/#disabled-by-default 130 | disable: 131 | # - asasalint 132 | # - asciicheck 133 | # - bidichk 134 | # - bodyclose 135 | # - containedctx 136 | - contextcheck 137 | # - cyclop 138 | # - decorder 139 | - depguard 140 | # - dogsled 141 | # - dupl 142 | # - dupword 143 | # - durationcheck 144 | # - errcheck 145 | # - errchkjson 146 | # - errname 147 | # - errorlint 148 | # - exhaustive 149 | - exhaustruct 150 | # - forbidigo 151 | # - forcetypeassert 152 | # - funlen 153 | - gci 154 | # - ginkgolinter 155 | # - gocheckcompilerdirectives 156 | - gochecknoglobals 157 | # - gochecknoinits 158 | # - gocognit 159 | # - goconst 160 | # - gocritic 161 | # - gocyclo 162 | - godot 163 | - godox 164 | # - gofmt 165 | # - gofumpt 166 | # - goheader 167 | # - goimports 168 | # - gomoddirectives 169 | # - gomodguard 170 | # - goprintffuncname 171 | - gosec 172 | # - gosimple 173 | - gosmopolitan 174 | # - govet 175 | # - grouper 176 | # - importas 177 | # - ineffassign 178 | # - interfacebloat 179 | # - interfacer 180 | # - ireturn 181 | # - lll 182 | # - loggercheck 183 | # - maintidx 184 | # - makezero 185 | # - mirror 186 | - misspell 187 | # - musttag 188 | # - nakedret 189 | - nestif 190 | # - nilerr 191 | # - nilnil 192 | # - nlreturn 193 | # - noctx 194 | # - nolintlint 195 | # - nonamedreturns 196 | # - nosprintfhostport 197 | # - paralleltest 198 | # - prealloc 199 | # - predeclared 200 | # - promlinter 201 | # - reassign 202 | # - revive 203 | # - rowserrcheck 204 | # - sqlclosecheck 205 | # - staticcheck 206 | # - stylecheck 207 | - tagalign 208 | - tagliatelle 209 | # - tenv 210 | # - testableexamples 211 | # - testpackage 212 | # - thelper 213 | # - tparallel 214 | # - typecheck 215 | # - unconvert 216 | # - unparam 217 | # - unused 218 | # - usestdlibvars 219 | - varnamelen 220 | # - wastedassign 221 | # - whitespace 222 | # - wrapcheck 223 | # - wsl 224 | # - zerologlint 225 | # Enable presets. 226 | # https://golangci-lint.run/usage/linters 227 | presets: 228 | - bugs 229 | - comment 230 | - complexity 231 | - error 232 | - format 233 | - import 234 | - metalinter 235 | - module 236 | - performance 237 | - sql 238 | - style 239 | - test 240 | - unused 241 | # Run only fast linters from enabled linters set (first run won't be fast) 242 | # Default: false 243 | fast: true 244 | -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=go 2 | GOTEST=$(GOCMD) test 3 | GOCLEAN=$(GOCMD) clean 4 | GOMOD=$(GOCMD) mod 5 | GOLINT_CMD=golangci-lint 6 | GOLINT_BASE_RUN=$(GOLINT_CMD) run 7 | GOLINT_RUN=$(GOLINT_BASE_RUN) 8 | BINARY_DIR=bin 9 | DEADCODE_RUN=deadcode ./... 10 | 11 | .PHONY: deps 12 | deps: 13 | $(GOMOD) tidy 14 | $(GOMOD) vendor 15 | .PHONY: lint 16 | lint: 17 | make -f tools/Makefile install-golangci 18 | $(GOLINT_RUN) 19 | 20 | .PHONY: lint-fix 21 | lint-fix: 22 | $(GOLINT_RUN) --fix 23 | .PHONY: clean 24 | clean: 25 | rm -rf $(BINARY_DIR) 26 | $(GOCLEAN) -cache -modcache # optional 27 | .PHONY: tools 28 | tools: 29 | make -f tools/Makefile 30 | .PHONY: format 31 | format: 32 | make -f tools/Makefile install-gofumpt 33 | gofumpt -l -w . 34 | 35 | .PHONY: mock 36 | mock: 37 | make -f tools/Makefile install-mockery 38 | go generate 39 | .PHONY: deadcode 40 | deadcode: 41 | make -f tools/Makefile install-deadcode 42 | $(DEADCODE_RUN) -------------------------------------------------------------------------------- /backend/cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "faladev/config" 5 | "faladev/internal/repository" 6 | "faladev/internal/services" 7 | "faladev/pkg/http" 8 | "fmt" 9 | "os" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "faladev/internal/database" 14 | 15 | "golang.org/x/oauth2" 16 | ) 17 | 18 | // @title Faladev API 19 | // @version 1.0 20 | // @description Esta é uma API exemplo do Faladev, que integra com o Google Calendar e o Gmail. 21 | // @termsOfService http://swagger.io/terms/ 22 | 23 | // @contact.name Suporte da API Faladev 24 | // @contact.url http://www.faladev.com/support 25 | // @contact.email support@faladev.com 26 | 27 | // @license.name Apache 2.0 28 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 29 | 30 | // @host localhost:8080 31 | // @BasePath / 32 | 33 | // @securityDefinitions.oauth2 OAuth2 34 | // @securityDefinitions.oauth2.description OAuth2 authorization for accessing the API 35 | // @securityDefinitions.oauth2.flow accessCode 36 | 37 | // @externalDocs.description OpenAPI Specification 38 | // @externalDocs.url https://swagger.io/resources/open-api/ 39 | func main() { 40 | 41 | log.SetOutput(os.Stdout) 42 | log.SetLevel(log.DebugLevel) 43 | 44 | appConfig, err := config.LoadConfig() 45 | 46 | if err != nil { 47 | log.Fatalf("Failed to load configuration: %v", err) 48 | } 49 | 50 | appOAuth2Config := appConfig.OAuth2.Config 51 | 52 | db, err := database.InitDB(appConfig.DatabaseURL) 53 | 54 | if err != nil { 55 | log.Fatalf("Failed to initialize database: %v", err) 56 | } 57 | 58 | calendarService := services.NewGoogleCalendarService() 59 | 60 | tokenRepo := repository.NewTokenRepository(db) 61 | studentRepo := repository.NewStudentRepository(db) 62 | eventRepo := repository.NewEventRepository(db) 63 | 64 | tokenService := services.NewTokenService(tokenRepo) 65 | studentService := services.NewStudentService(studentRepo) 66 | eventService := services.NewEventService(eventRepo, calendarService) 67 | 68 | token, err := tokenService.GetToken() 69 | 70 | if err != nil { 71 | log.Fatalf("Failed to load token: %v", err) 72 | } 73 | 74 | if token == nil { 75 | log.Println("Token not found, redirecting to authentication...") 76 | fmt.Println("Please visit the following link to authorize your Google account: ", appOAuth2Config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)) 77 | } 78 | 79 | emailService := services.NewGmailService(appOAuth2Config, token) 80 | 81 | app := http.NewApp(appOAuth2Config, calendarService, emailService, *studentService, *tokenService, *eventService) 82 | 83 | app.StartServer() 84 | 85 | } 86 | -------------------------------------------------------------------------------- /backend/cmd/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Esta é uma API exemplo do Faladev, que integra com o Google Calendar e o Gmail.", 5 | "title": "Faladev API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "Suporte da API Faladev", 9 | "url": "http://www.faladev.com/support", 10 | "email": "support@faladev.com" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | }, 16 | "version": "1.0" 17 | }, 18 | "host": "localhost:8080", 19 | "basePath": "/", 20 | "paths": { 21 | "/": { 22 | "get": { 23 | "description": "Renders the form.html page to display user information form.", 24 | "produces": [ 25 | "text/html" 26 | ], 27 | "summary": "Render form page", 28 | "responses": { 29 | "200": { 30 | "description": "HTML content of the form page", 31 | "schema": { 32 | "type": "string" 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "/callback": { 39 | "get": { 40 | "description": "Exchange code for token and save it", 41 | "consumes": [ 42 | "application/json" 43 | ], 44 | "produces": [ 45 | "application/json" 46 | ], 47 | "summary": "Handle OAuth2 callback", 48 | "parameters": [ 49 | { 50 | "type": "string", 51 | "description": "State token", 52 | "name": "state", 53 | "in": "query", 54 | "required": true 55 | }, 56 | { 57 | "type": "string", 58 | "description": "Authorization code", 59 | "name": "code", 60 | "in": "query", 61 | "required": true 62 | } 63 | ], 64 | "responses": { 65 | "303": { 66 | "description": "Redirects to /" 67 | }, 68 | "400": { 69 | "description": "State token doesn't match", 70 | "schema": { 71 | "$ref": "#/definitions/http.ErrorResponse" 72 | } 73 | }, 74 | "500": { 75 | "description": "Unable to retrieve or save token", 76 | "schema": { 77 | "$ref": "#/definitions/http.ErrorResponse" 78 | } 79 | } 80 | } 81 | } 82 | }, 83 | "/event": { 84 | "post": { 85 | "description": "Handles event creation, guest addition, email sending, and redirects to Google Meet link.", 86 | "consumes": [ 87 | "application/json" 88 | ], 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "summary": "Handle event creation and interaction", 93 | "parameters": [ 94 | { 95 | "type": "string", 96 | "description": "Name of the student", 97 | "name": "name", 98 | "in": "formData", 99 | "required": true 100 | }, 101 | { 102 | "type": "string", 103 | "description": "Email of the student", 104 | "name": "email", 105 | "in": "formData", 106 | "required": true 107 | }, 108 | { 109 | "type": "string", 110 | "description": "Phone number of the student", 111 | "name": "phone", 112 | "in": "formData", 113 | "required": true 114 | } 115 | ], 116 | "responses": { 117 | "303": { 118 | "description": "Redirects to Google Meet link", 119 | "schema": { 120 | "type": "string" 121 | } 122 | }, 123 | "400": { 124 | "description": "No Google Meet link available or other errors", 125 | "schema": { 126 | "$ref": "#/definitions/http.ErrorResponse" 127 | } 128 | }, 129 | "500": { 130 | "description": "Internal server error", 131 | "schema": { 132 | "$ref": "#/definitions/http.ErrorResponse" 133 | } 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "definitions": { 140 | "http.ErrorResponse": { 141 | "type": "object", 142 | "properties": { 143 | "error": { 144 | "type": "string" 145 | } 146 | } 147 | } 148 | }, 149 | "externalDocs": { 150 | "description": "OpenAPI Specification", 151 | "url": "https://swagger.io/resources/open-api/" 152 | } 153 | } -------------------------------------------------------------------------------- /backend/cmd/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | http.ErrorResponse: 4 | properties: 5 | error: 6 | type: string 7 | type: object 8 | externalDocs: 9 | description: OpenAPI Specification 10 | url: https://swagger.io/resources/open-api/ 11 | host: localhost:8080 12 | info: 13 | contact: 14 | email: support@faladev.com 15 | name: Suporte da API Faladev 16 | url: http://www.faladev.com/support 17 | description: Esta é uma API exemplo do Faladev, que integra com o Google Calendar 18 | e o Gmail. 19 | license: 20 | name: Apache 2.0 21 | url: http://www.apache.org/licenses/LICENSE-2.0.html 22 | termsOfService: http://swagger.io/terms/ 23 | title: Faladev API 24 | version: "1.0" 25 | paths: 26 | /: 27 | get: 28 | description: Renders the form.html page to display user information form. 29 | produces: 30 | - text/html 31 | responses: 32 | "200": 33 | description: HTML content of the form page 34 | schema: 35 | type: string 36 | summary: Render form page 37 | /callback: 38 | get: 39 | consumes: 40 | - application/json 41 | description: Exchange code for token and save it 42 | parameters: 43 | - description: State token 44 | in: query 45 | name: state 46 | required: true 47 | type: string 48 | - description: Authorization code 49 | in: query 50 | name: code 51 | required: true 52 | type: string 53 | produces: 54 | - application/json 55 | responses: 56 | "303": 57 | description: Redirects to / 58 | "400": 59 | description: State token doesn't match 60 | schema: 61 | $ref: '#/definitions/http.ErrorResponse' 62 | "500": 63 | description: Unable to retrieve or save token 64 | schema: 65 | $ref: '#/definitions/http.ErrorResponse' 66 | summary: Handle OAuth2 callback 67 | /event: 68 | post: 69 | consumes: 70 | - application/json 71 | description: Handles event creation, guest addition, email sending, and redirects 72 | to Google Meet link. 73 | parameters: 74 | - description: Name of the student 75 | in: formData 76 | name: name 77 | required: true 78 | type: string 79 | - description: Email of the student 80 | in: formData 81 | name: email 82 | required: true 83 | type: string 84 | - description: Phone number of the student 85 | in: formData 86 | name: phone 87 | required: true 88 | type: string 89 | produces: 90 | - application/json 91 | responses: 92 | "303": 93 | description: Redirects to Google Meet link 94 | schema: 95 | type: string 96 | "400": 97 | description: No Google Meet link available or other errors 98 | schema: 99 | $ref: '#/definitions/http.ErrorResponse' 100 | "500": 101 | description: Internal server error 102 | schema: 103 | $ref: '#/definitions/http.ErrorResponse' 104 | summary: Handle event creation and interaction 105 | swagger: "2.0" 106 | -------------------------------------------------------------------------------- /backend/cmd/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "faladev/config" 5 | "faladev/internal/database" 6 | "faladev/internal/models" 7 | "fmt" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | 13 | fmt.Println("Starting migration...") 14 | 15 | appConfig, err := config.LoadConfig() 16 | 17 | if err != nil { 18 | log.Fatalf("Failed to load configuration: %v", err) 19 | } 20 | 21 | db, err := database.InitDB(appConfig.DatabaseURL) 22 | 23 | if err != nil { 24 | log.Fatalf("Failed to initialize database: %v", err) 25 | } 26 | 27 | db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";") 28 | 29 | errStudents := db.AutoMigrate(&models.Student{}) 30 | 31 | if errStudents != nil { 32 | log.Fatalf("Failed to migrate students: %v", errStudents) 33 | } 34 | 35 | errToken := db.AutoMigrate(&models.Token{}) 36 | 37 | if errToken != nil { 38 | log.Fatalf("Failed to migrate tokens: %v", errToken) 39 | } 40 | 41 | errEvent := db.AutoMigrate(&models.Event{}) 42 | 43 | if errEvent != nil { 44 | log.Fatalf("Failed to migrate events: %v", errEvent) 45 | } 46 | 47 | fmt.Println("Migration completed successfully!") 48 | 49 | database.Seed(db) 50 | 51 | fmt.Println("Seed completed successfully!") 52 | } 53 | -------------------------------------------------------------------------------- /backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | "golang.org/x/oauth2" 10 | "golang.org/x/oauth2/google" 11 | "google.golang.org/api/calendar/v3" 12 | "google.golang.org/api/gmail/v1" 13 | ) 14 | 15 | type OAuth2Config struct { 16 | Config *oauth2.Config 17 | Token *oauth2.Token 18 | } 19 | 20 | type Config struct { 21 | DatabaseURL string 22 | OAuth2 OAuth2Config 23 | } 24 | 25 | func getDatabaseURL() string { 26 | databaseURL := os.Getenv("DATABASE_URL") 27 | if databaseURL == "" { 28 | log.Fatal("Please set the DATABASE_URL environment variable") 29 | } 30 | return databaseURL 31 | } 32 | 33 | func getOAuthConfig() *oauth2.Config { 34 | 35 | clientID := os.Getenv("GOOGLE_CLIENT_ID") 36 | clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") 37 | redirectURL := os.Getenv("GOOGLE_REDIRECT_URL") 38 | 39 | if clientID == "" || clientSecret == "" || redirectURL == "" { 40 | log.Fatal("Please set the GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URL environment variables") 41 | } 42 | 43 | return &oauth2.Config{ 44 | ClientID: clientID, 45 | ClientSecret: clientSecret, 46 | RedirectURL: redirectURL, 47 | Scopes: []string{gmail.GmailSendScope, calendar.CalendarScope}, 48 | Endpoint: google.Endpoint, 49 | } 50 | } 51 | 52 | func LoadConfig() (*Config, error) { 53 | 54 | err := godotenv.Load() 55 | 56 | if err != nil { 57 | return nil, errors.New("failed to load environment variables") 58 | } 59 | 60 | oauthConfig := getOAuthConfig() 61 | databaseURL := getDatabaseURL() 62 | 63 | return &Config{ 64 | DatabaseURL: databaseURL, 65 | OAuth2: OAuth2Config{Config: oauthConfig}, 66 | }, nil 67 | } 68 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module faladev 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/google/uuid v1.6.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/lib/pq v1.10.9 10 | github.com/pkg/errors v0.9.1 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/stretchr/testify v1.9.0 13 | github.com/swaggo/files v1.0.1 14 | github.com/swaggo/gin-swagger v1.6.0 15 | github.com/swaggo/swag v1.16.3 16 | golang.org/x/oauth2 v0.21.0 17 | google.golang.org/api v0.186.0 18 | gorm.io/driver/postgres v1.5.9 19 | gorm.io/gorm v1.25.10 20 | ) 21 | 22 | require ( 23 | cloud.google.com/go/auth v0.6.0 // indirect 24 | cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect 25 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 26 | github.com/KyleBanks/depth v1.2.1 // indirect 27 | github.com/bytedance/sonic v1.11.9 // indirect 28 | github.com/bytedance/sonic/loader v0.1.1 // indirect 29 | github.com/cloudwego/base64x v0.1.4 // indirect 30 | github.com/cloudwego/iasm v0.2.0 // indirect 31 | github.com/davecgh/go-spew v1.1.1 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect 34 | github.com/gin-contrib/cors v1.7.2 // indirect 35 | github.com/gin-contrib/sse v0.1.0 // indirect 36 | github.com/go-logr/logr v1.4.2 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 39 | github.com/go-openapi/jsonreference v0.21.0 // indirect 40 | github.com/go-openapi/spec v0.21.0 // indirect 41 | github.com/go-openapi/swag v0.23.0 // indirect 42 | github.com/go-playground/locales v0.14.1 // indirect 43 | github.com/go-playground/universal-translator v0.18.1 // indirect 44 | github.com/go-playground/validator/v10 v10.22.0 // indirect 45 | github.com/goccy/go-json v0.10.3 // indirect 46 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 47 | github.com/golang/protobuf v1.5.4 // indirect 48 | github.com/google/s2a-go v0.1.7 // indirect 49 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 50 | github.com/googleapis/gax-go/v2 v2.12.5 // indirect 51 | github.com/jackc/pgpassfile v1.0.0 // indirect 52 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 53 | github.com/jackc/pgx/v5 v5.6.0 // indirect 54 | github.com/jackc/puddle/v2 v2.2.1 // indirect 55 | github.com/jinzhu/inflection v1.0.0 // indirect 56 | github.com/jinzhu/now v1.1.5 // indirect 57 | github.com/josharian/intern v1.0.0 // indirect 58 | github.com/json-iterator/go v1.1.12 // indirect 59 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 60 | github.com/leodido/go-urn v1.4.0 // indirect 61 | github.com/mailru/easyjson v0.7.7 // indirect 62 | github.com/mattn/go-isatty v0.0.20 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 66 | github.com/pmezard/go-difflib v1.0.0 // indirect 67 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 68 | github.com/ugorji/go/codec v1.2.12 // indirect 69 | go.opencensus.io v0.24.0 // indirect 70 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect 71 | go.opentelemetry.io/otel v1.27.0 // indirect 72 | go.opentelemetry.io/otel/metric v1.27.0 // indirect 73 | go.opentelemetry.io/otel/trace v1.27.0 // indirect 74 | golang.org/x/arch v0.8.0 // indirect 75 | golang.org/x/crypto v0.25.0 // indirect 76 | golang.org/x/net v0.27.0 // indirect 77 | golang.org/x/sync v0.7.0 // indirect 78 | golang.org/x/sys v0.22.0 // indirect 79 | golang.org/x/text v0.16.0 // indirect 80 | golang.org/x/tools v0.23.0 // indirect 81 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect 82 | google.golang.org/grpc v1.64.0 // indirect 83 | google.golang.org/protobuf v1.34.2 // indirect 84 | gopkg.in/yaml.v3 v3.0.1 // indirect 85 | ) 86 | -------------------------------------------------------------------------------- /backend/internal/auth/client.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | func CreateOAuthClient(ctx context.Context, oauthConfig *oauth2.Config, token *oauth2.Token) (*http.Client, error) { 12 | client := oauthConfig.Client(ctx, token) 13 | if client == nil { 14 | return nil, errors.New("error creating OAuth client") 15 | } 16 | return client, nil 17 | } 18 | -------------------------------------------------------------------------------- /backend/internal/auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "golang.org/x/oauth2" 8 | ) 9 | 10 | func ExchangeCodeForToken(ctx context.Context, oauthConfig *oauth2.Config, code string) (*oauth2.Token, error) { 11 | token, err := oauthConfig.Exchange(ctx, code) 12 | if err != nil { 13 | return nil, fmt.Errorf("unable to retrieve token from web: %v", err) 14 | } 15 | return token, nil 16 | } 17 | 18 | func RefreshToken(ctx context.Context, oauthConfig *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) { 19 | ts := oauthConfig.TokenSource(ctx, token) 20 | newToken, err := ts.Token() 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to refresh token: %v", err) 23 | } 24 | return newToken, nil 25 | } 26 | -------------------------------------------------------------------------------- /backend/internal/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "time" 7 | 8 | _ "github.com/lib/pq" 9 | 10 | "gorm.io/driver/postgres" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | var ( 15 | db *gorm.DB 16 | once sync.Once 17 | ) 18 | 19 | func InitDB(databaseURL string) (*gorm.DB, error) { 20 | 21 | var err error 22 | 23 | once.Do(func() { 24 | 25 | db, err = gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) 26 | if err != nil { 27 | log.Printf("failed to connect to database: %v", err) 28 | return 29 | } 30 | 31 | sqlDB, err := db.DB() 32 | if err != nil { 33 | log.Printf("failed to get database connection handle: %v", err) 34 | return 35 | } 36 | 37 | sqlDB.SetMaxIdleConns(20) 38 | sqlDB.SetMaxOpenConns(200) 39 | sqlDB.SetConnMaxLifetime(time.Hour) 40 | }) 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return db, nil 47 | } 48 | -------------------------------------------------------------------------------- /backend/internal/database/seed.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "faladev/internal/models" 6 | "faladev/internal/repository" 7 | "faladev/internal/services" 8 | "log" 9 | "time" 10 | 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func Seed(db *gorm.DB) { 15 | 16 | eventRepo := repository.NewEventRepository(db) 17 | 18 | calendarService := services.NewGoogleCalendarService() 19 | 20 | eventService := services.NewEventService(eventRepo, calendarService) 21 | 22 | seedEvents(eventService) 23 | } 24 | 25 | func seedEvents(eventService *services.EventService) { 26 | 27 | ctx := context.Background() 28 | 29 | eventCount, err := eventService.CountEvents() 30 | 31 | if err != nil { 32 | log.Fatalf("Failed to count events: %v", err) 33 | } 34 | 35 | if eventCount == 0 { 36 | 37 | log.Println("Inserting default event...") 38 | 39 | defaultEvent := models.Event{ 40 | Name: "Mentoria (Carreira e Tecnologia)", 41 | Key: "04mti3liihmd9u2agf8hg7kf6u", 42 | Description: "Essa é uma mentoria gratuita para quem está entrando na área de tecnologia, migrando de área ou buscando crescimento profissional.", 43 | Location: "https://meet.google.com/eam-bqde-mgd", 44 | StartDate: time.Now().Add(24 * time.Hour), 45 | EndDate: time.Now().Add(26 * time.Hour), 46 | StartTime: time.Now().Add(24 * time.Hour), 47 | EndTime: time.Now().Add(26 * time.Hour), 48 | Organizer: "Marcos Fonseca", 49 | Email: "mentoria@faladev.tech", 50 | Phone: "", 51 | CalendarEventLink: "https://www.google.com/calendar/event?eid=MDRtdGkzbGlpaG1kOXUyYWdmOGhnN2tmNnVfMjAyNDA2MTJUMjIwMDAwWiBjb250YXRvQG1hcmNvc2ZvbnNlY2EuY29tLmJy", 52 | } 53 | 54 | err = eventService.CreateEvent(ctx, nil, &defaultEvent) 55 | 56 | if err != nil { 57 | log.Fatalf("Failed to insert default event: %v", err) 58 | } 59 | 60 | log.Println("Default event inserted successfully!") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/internal/models/events.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Event struct { 10 | gorm.Model 11 | Name string `json:"name"` 12 | Key string `json:"key"` 13 | Description string `json:"description"` 14 | Location string `json:"location"` 15 | StartDate time.Time `json:"start_date"` 16 | EndDate time.Time `json:"end_date"` 17 | StartTime time.Time `json:"start_time"` 18 | EndTime time.Time `json:"end_time"` 19 | Organizer string `json:"organizer"` 20 | Email string `json:"email"` 21 | Phone string `json:"phone"` 22 | CalendarEventLink string `json:"calendar_event_link"` 23 | } 24 | -------------------------------------------------------------------------------- /backend/internal/models/student.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Student struct { 8 | gorm.Model 9 | Name string 10 | Email string 11 | Phone string 12 | } 13 | -------------------------------------------------------------------------------- /backend/internal/models/token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Token struct { 8 | gorm.Model 9 | Token string 10 | } 11 | -------------------------------------------------------------------------------- /backend/internal/repository/event_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "faladev/internal/models" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type EventRepository struct { 10 | db *gorm.DB 11 | } 12 | 13 | func NewEventRepository(db *gorm.DB) *EventRepository { 14 | return &EventRepository{ 15 | db: db, 16 | } 17 | } 18 | 19 | func (eventRepository *EventRepository) CreateEvent(event *models.Event) error { 20 | return eventRepository.db.Create(event).Error 21 | } 22 | 23 | func (eventRepository *EventRepository) GetEventByID(id uint) (*models.Event, error) { 24 | var event models.Event 25 | err := eventRepository.db.First(&event, id).Error 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &event, nil 30 | } 31 | 32 | func (eventRepository *EventRepository) UpdateEvent(event *models.Event) error { 33 | return eventRepository.db.Save(event).Error 34 | } 35 | 36 | func (eventRepository *EventRepository) DeleteEvent(id uint) error { 37 | return eventRepository.db.Delete(&models.Event{}, id).Error 38 | } 39 | 40 | func (eventRepository *EventRepository) ListEvents() ([]models.Event, error) { 41 | var events []models.Event 42 | err := eventRepository.db.Find(&events).Error 43 | if err != nil { 44 | return nil, err 45 | } 46 | return events, nil 47 | } 48 | 49 | func (eventRepository *EventRepository) GetNextEvent() (*models.Event, error) { 50 | var event models.Event 51 | err := eventRepository.db. 52 | //Where("start_date >= ?", time.Now()). 53 | Order("start_date, start_time"). 54 | Debug(). 55 | First(&event).Error 56 | if err != nil { 57 | return nil, err 58 | } 59 | return &event, nil 60 | } 61 | 62 | func (eventRepository *EventRepository) CountEvents() (int64, error) { 63 | var count int64 64 | err := eventRepository.db.Model(&models.Event{}).Count(&count).Error 65 | if err != nil { 66 | return 0, err 67 | } 68 | return count, nil 69 | } 70 | -------------------------------------------------------------------------------- /backend/internal/repository/student_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "faladev/internal/models" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type StudentRepository struct { 10 | db *gorm.DB 11 | } 12 | 13 | func NewStudentRepository(db *gorm.DB) *StudentRepository { 14 | return &StudentRepository{ 15 | db: db, 16 | } 17 | } 18 | 19 | func (studentRepository *StudentRepository) InsertOrUpdateStudent(name, email, phone string) error { 20 | 21 | student := &models.Student{Name: name, Email: email, Phone: phone} 22 | 23 | err := studentRepository.db.Where("email = ?", email).FirstOrCreate(&student).Error 24 | 25 | if err != nil { 26 | return err 27 | } 28 | 29 | student.Name = name 30 | student.Phone = phone 31 | 32 | result := studentRepository.db.Save(&student) 33 | 34 | return result.Error 35 | } 36 | -------------------------------------------------------------------------------- /backend/internal/repository/token_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | "faladev/internal/models" 6 | 7 | "golang.org/x/oauth2" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type TokenRepository struct { 12 | db *gorm.DB 13 | } 14 | 15 | func NewTokenRepository(db *gorm.DB) *TokenRepository { 16 | return &TokenRepository{ 17 | db: db, 18 | } 19 | } 20 | 21 | func (tokenRepository *TokenRepository) CreateToken(token *oauth2.Token) error { 22 | 23 | tokenJSON, err := json.Marshal(token) 24 | 25 | if err != nil { 26 | return err 27 | } 28 | 29 | newToken := models.Token{ 30 | Token: string(tokenJSON), 31 | } 32 | 33 | return tokenRepository.db.Create(&newToken).Error 34 | } 35 | 36 | func (tokenRepository *TokenRepository) GetToken() (*oauth2.Token, error) { 37 | 38 | var tokenModel models.Token 39 | 40 | err := tokenRepository.db.Order("created_at desc").First(&tokenModel).Error 41 | 42 | if err != nil { 43 | if err == gorm.ErrRecordNotFound { 44 | return nil, nil 45 | } 46 | return nil, err 47 | } 48 | 49 | var token oauth2.Token 50 | 51 | err = json.Unmarshal([]byte(tokenModel.Token), &token) 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return &token, nil 58 | } 59 | -------------------------------------------------------------------------------- /backend/internal/services/calendar_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "golang.org/x/oauth2" 7 | "google.golang.org/api/calendar/v3" 8 | ) 9 | 10 | type CalendarService interface { 11 | InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) 12 | AddGuestToEvent(ctx context.Context, service CalendarAPI, eventKey, email string) (*calendar.Event, error) 13 | FindEventByKey(ctx context.Context, service CalendarAPI, eventKey string) (*calendar.Event, error) 14 | CreateEvent(ctx context.Context, service CalendarAPI, event *calendar.Event) (*calendar.Event, error) 15 | } 16 | 17 | type CalendarAPI interface { 18 | EventsList(calendarID string) EventsListCall 19 | GetEvent(calendarID, eventID string) EventCall 20 | UpdateEvent(calendarID, eventID string, event *calendar.Event) EventCall 21 | InsertEvent(calendarID string, event *calendar.Event) EventCall 22 | } 23 | 24 | type EventsListCall interface { 25 | Do() (*calendar.Events, error) 26 | } 27 | 28 | type EventCall interface { 29 | Do() (*calendar.Event, error) 30 | } 31 | 32 | type RealCalendarService struct { 33 | CalendarService *calendar.Service 34 | } 35 | 36 | func (rcs *RealCalendarService) EventsList(calendarID string) EventsListCall { 37 | return &realEventsListCall{ 38 | call: rcs.CalendarService.Events.List(calendarID), 39 | } 40 | } 41 | 42 | func (rcs *RealCalendarService) GetEvent(calendarID, eventID string) EventCall { 43 | return &realEventCall{ 44 | getCall: rcs.CalendarService.Events.Get(calendarID, eventID), 45 | } 46 | } 47 | 48 | func (rcs *RealCalendarService) UpdateEvent(calendarID, eventID string, event *calendar.Event) EventCall { 49 | return &realUpdateEventCall{ 50 | updateCall: rcs.CalendarService.Events.Update(calendarID, eventID, event), 51 | } 52 | } 53 | 54 | func (rcs *RealCalendarService) InsertEvent(calendarID string, event *calendar.Event) EventCall { 55 | return &realInsertEventCall{insertCall: rcs.CalendarService.Events.Insert(calendarID, event)} 56 | } 57 | 58 | type realEventsListCall struct { 59 | call *calendar.EventsListCall 60 | } 61 | 62 | func (rel *realEventsListCall) Do() (*calendar.Events, error) { 63 | return rel.call.Do() 64 | } 65 | 66 | type realEventCall struct { 67 | getCall *calendar.EventsGetCall 68 | } 69 | 70 | func (rec *realEventCall) Do() (*calendar.Event, error) { 71 | return rec.getCall.Do() 72 | } 73 | 74 | type realUpdateEventCall struct { 75 | updateCall *calendar.EventsUpdateCall 76 | } 77 | 78 | func (ruc *realUpdateEventCall) Do() (*calendar.Event, error) { 79 | return ruc.updateCall.Do() 80 | } 81 | 82 | type realInsertEventCall struct { 83 | insertCall *calendar.EventsInsertCall 84 | } 85 | 86 | func (ric *realInsertEventCall) Do() (*calendar.Event, error) { 87 | return ric.insertCall.ConferenceDataVersion(1).Do() 88 | } 89 | -------------------------------------------------------------------------------- /backend/internal/services/email_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "faladev/internal/models" 5 | 6 | "golang.org/x/oauth2" 7 | ) 8 | 9 | type EmailService interface { 10 | SendInvite(recipient string, eventDetails *models.Event, token *oauth2.Token) error 11 | } 12 | -------------------------------------------------------------------------------- /backend/internal/services/event_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "faladev/internal/models" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | "google.golang.org/api/calendar/v3" 10 | ) 11 | 12 | type EventRepository interface { 13 | CreateEvent(event *models.Event) error 14 | GetEventByID(id uint) (*models.Event, error) 15 | UpdateEvent(event *models.Event) error 16 | DeleteEvent(id uint) error 17 | ListEvents() ([]models.Event, error) 18 | GetNextEvent() (*models.Event, error) 19 | CountEvents() (int64, error) 20 | } 21 | 22 | type EventService struct { 23 | repo EventRepository 24 | calendarService CalendarService 25 | } 26 | 27 | func NewEventService(repo EventRepository, calendarService CalendarService) *EventService { 28 | return &EventService{ 29 | repo: repo, 30 | calendarService: calendarService, 31 | } 32 | } 33 | 34 | func (eventService *EventService) CreateEvent(ctx context.Context, api *CalendarAPI, event *models.Event) error { 35 | 36 | if api == nil { 37 | return nil 38 | } 39 | 40 | eventCalendar := &calendar.Event{ 41 | Summary: event.Name, 42 | Description: event.Description, 43 | Start: &calendar.EventDateTime{ 44 | DateTime: event.StartTime.Format(time.RFC3339), 45 | TimeZone: "America/Sao_Paulo", 46 | }, 47 | End: &calendar.EventDateTime{ 48 | DateTime: event.EndTime.Format(time.RFC3339), 49 | TimeZone: "America/Sao_Paulo", 50 | }, 51 | ConferenceData: &calendar.ConferenceData{ 52 | CreateRequest: &calendar.CreateConferenceRequest{ 53 | RequestId: uuid.New().String(), 54 | ConferenceSolutionKey: &calendar.ConferenceSolutionKey{ 55 | Type: "hangoutsMeet", 56 | }, 57 | }, 58 | }, 59 | } 60 | 61 | newEvent, err := eventService.calendarService.CreateEvent(ctx, *api, eventCalendar) 62 | 63 | if err != nil { 64 | return err 65 | } 66 | 67 | event.Key = newEvent.Id 68 | event.Location = newEvent.HangoutLink 69 | event.CalendarEventLink = newEvent.HtmlLink 70 | 71 | if err := eventService.repo.CreateEvent(event); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (eventService *EventService) GetEventByID(id uint) (*models.Event, error) { 79 | return eventService.repo.GetEventByID(id) 80 | } 81 | 82 | func (eventService *EventService) UpdateEvent(event *models.Event) error { 83 | return eventService.repo.UpdateEvent(event) 84 | } 85 | 86 | func (eventService *EventService) DeleteEvent(id uint) error { 87 | return eventService.repo.DeleteEvent(id) 88 | } 89 | 90 | func (eventService *EventService) ListEvents() ([]models.Event, error) { 91 | return eventService.repo.ListEvents() 92 | } 93 | 94 | func (eventService *EventService) GetNextEvent() (*models.Event, error) { 95 | return eventService.repo.GetNextEvent() 96 | } 97 | 98 | func (eventService *EventService) CountEvents() (int64, error) { 99 | return eventService.repo.CountEvents() 100 | } 101 | -------------------------------------------------------------------------------- /backend/internal/services/gmail_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "faladev/internal/models" 7 | "faladev/pkg/utils" 8 | "fmt" 9 | "html/template" 10 | 11 | "golang.org/x/oauth2" 12 | "google.golang.org/api/gmail/v1" 13 | "google.golang.org/api/option" 14 | ) 15 | 16 | type GmailService struct { 17 | config *oauth2.Config 18 | token *oauth2.Token 19 | } 20 | 21 | func NewGmailService(config *oauth2.Config, token *oauth2.Token) EmailService { 22 | return &GmailService{ 23 | config: config, 24 | token: token, 25 | } 26 | } 27 | 28 | func (gmailService *GmailService) SendInvite(recipient string, eventDetails *models.Event, token *oauth2.Token) error { 29 | 30 | ctx := context.Background() 31 | 32 | client := gmailService.config.Client(ctx, token) 33 | 34 | srv, err := gmail.NewService(ctx, option.WithHTTPClient(client)) 35 | 36 | if err != nil { 37 | return fmt.Errorf("unable to retrieve Gmail client: %w", err) 38 | } 39 | 40 | emailFrom := fmt.Sprintf("%s <%s>", eventDetails.Organizer, eventDetails.Email) 41 | subject := fmt.Sprintf("Convite para %s", eventDetails.Name) 42 | 43 | body, err := gmailService.getEmailBody("templates/email/mentorship.html", eventDetails) 44 | 45 | if err != nil { 46 | return fmt.Errorf("failed to compose email body: %w", err) 47 | } 48 | 49 | fullEmail := gmailService.buildEmailMessage(emailFrom, recipient, subject, body) 50 | 51 | encodedEmail := utils.Base64URLEncode([]byte(fullEmail)) 52 | 53 | message := &gmail.Message{ 54 | Raw: encodedEmail, 55 | } 56 | 57 | _, err = srv.Users.Messages.Send("me", message).Do() 58 | 59 | if err != nil { 60 | return fmt.Errorf("failed to send email: %w", err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (gmailService *GmailService) getEmailBody(templatePath string, eventDetails *models.Event) (string, error) { 67 | 68 | tmpl, err := template.ParseFiles(templatePath) 69 | 70 | if err != nil { 71 | return "", fmt.Errorf("error loading template: %w", err) 72 | } 73 | 74 | var body bytes.Buffer 75 | 76 | if err := tmpl.Execute(&body, eventDetails); err != nil { 77 | return "", fmt.Errorf("error executing template: %w", err) 78 | } 79 | 80 | return body.String(), nil 81 | } 82 | 83 | func (gmailService *GmailService) buildEmailMessage(from, to, subject, body string) string { 84 | return fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\r\n%s", 85 | from, to, subject, body) 86 | } 87 | -------------------------------------------------------------------------------- /backend/internal/services/google_calendar_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "faladev/internal/auth" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/pkg/errors" 10 | "golang.org/x/oauth2" 11 | "google.golang.org/api/calendar/v3" 12 | "google.golang.org/api/googleapi" 13 | "google.golang.org/api/option" 14 | ) 15 | 16 | type GoogleCalendarService struct{} 17 | 18 | func NewGoogleCalendarService() CalendarService { 19 | return &GoogleCalendarService{} 20 | } 21 | 22 | func (googleCalendarService *GoogleCalendarService) InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) { 23 | 24 | client, err := auth.CreateOAuthClient(ctx, config, token) 25 | 26 | if err != nil { 27 | return nil, fmt.Errorf("error creating OAuth client: %v", err) 28 | } 29 | 30 | service, err := calendar.NewService(ctx, option.WithHTTPClient(client)) 31 | 32 | if err != nil { 33 | return nil, fmt.Errorf("error creating calendar service: %v", err) 34 | } 35 | return &RealCalendarService{CalendarService: service}, nil 36 | } 37 | 38 | func (googleCalendarService *GoogleCalendarService) FindEventByKey(ctx context.Context, api CalendarAPI, eventKey string) (*calendar.Event, error) { 39 | 40 | event, err := api.GetEvent("primary", eventKey).Do() 41 | 42 | if err != nil { 43 | return nil, errors.Wrap(err, fmt.Sprintf("error fetching event with eventKey %s", eventKey)) 44 | } 45 | 46 | return event, nil 47 | } 48 | 49 | func (googleCalendarService *GoogleCalendarService) AddGuestToEvent(ctx context.Context, api CalendarAPI, hangoutLink, email string) (*calendar.Event, error) { 50 | 51 | eventDetails, err := googleCalendarService.FindEventByKey(ctx, api, hangoutLink) 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | updatedEvent, err := api.GetEvent("primary", eventDetails.Id).Do() 58 | 59 | if err != nil { 60 | return nil, errors.Wrap(err, "error getting event details") 61 | } 62 | 63 | for _, attendee := range updatedEvent.Attendees { 64 | if attendee.Email == email { 65 | return updatedEvent, nil 66 | } 67 | } 68 | 69 | attendee := &calendar.EventAttendee{Email: email} 70 | 71 | updatedEvent.Attendees = append(updatedEvent.Attendees, attendee) 72 | 73 | _, err = api.UpdateEvent("primary", updatedEvent.Id, updatedEvent).Do() 74 | 75 | if err != nil { 76 | 77 | if gerr, ok := err.(*googleapi.Error); ok { 78 | switch gerr.Code { 79 | case http.StatusNotFound: 80 | return nil, errors.New("Event not found") 81 | case http.StatusForbidden: 82 | return nil, errors.New("Forbidden") 83 | default: 84 | return nil, errors.Wrap(err, "error updating event") 85 | } 86 | } else { 87 | return nil, errors.Wrap(err, "unexpected error") 88 | } 89 | } 90 | 91 | return updatedEvent, nil 92 | } 93 | 94 | func (googleCalendarService *GoogleCalendarService) CreateEvent(ctx context.Context, api CalendarAPI, newEvent *calendar.Event) (*calendar.Event, error) { 95 | 96 | event, err := api.InsertEvent("primary", newEvent).Do() 97 | 98 | if err != nil { 99 | if gerr, ok := err.(*googleapi.Error); ok { 100 | switch gerr.Code { 101 | case http.StatusForbidden: 102 | return nil, errors.New("Forbidden: Access to calendar denied") 103 | case http.StatusBadRequest: 104 | return nil, errors.New("Bad Request: Invalid event details") 105 | default: 106 | return nil, errors.Wrap(err, "error creating event") 107 | } 108 | } else { 109 | return nil, errors.Wrap(err, "unexpected error while creating event") 110 | } 111 | } 112 | return event, nil 113 | } 114 | -------------------------------------------------------------------------------- /backend/internal/services/linkedin_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/sirupsen/logrus" 10 | "golang.org/x/oauth2" 11 | "golang.org/x/oauth2/linkedin" 12 | ) 13 | 14 | const ( 15 | LINKEDIN_CLIENT_ID = "78ayhj9r8gh3ce" 16 | LINKEDIN_CLIENT_SECRET = "Tp7E0Gao0sf34NwY" 17 | redirectURL = "http://localhost:8080/callback" 18 | ) 19 | 20 | var ( 21 | oauth2Config = &oauth2.Config{ 22 | ClientID: LINKEDIN_CLIENT_ID, 23 | ClientSecret: LINKEDIN_CLIENT_SECRET, 24 | Endpoint: linkedin.Endpoint, 25 | RedirectURL: redirectURL, 26 | Scopes: []string{"r_liteprofile", "r_emailaddress"}, 27 | } 28 | logrusLogger = logrus.New() 29 | state = "abc123" 30 | ) 31 | 32 | // Handler to redirect the user to LinkedIn login page 33 | func LoginHandler(w http.ResponseWriter, r *http.Request) { 34 | url := oauth2Config.AuthCodeURL(state) 35 | logrusLogger.Debugf("Redirecting to %s", url) 36 | http.Redirect(w, r, url, http.StatusFound) 37 | } 38 | 39 | // Handler for LinkedIn OAuth callback 40 | func CallbackHandler(w http.ResponseWriter, r *http.Request) { 41 | getstate := r.FormValue("state") 42 | if getstate != state { 43 | logrusLogger.Errorf("Invalid state: %s", state) 44 | http.Error(w, "Invalid state", http.StatusBadRequest) 45 | return 46 | } 47 | 48 | code := r.FormValue("code") 49 | token, err := oauth2Config.Exchange(context.Background(), code) 50 | if err != nil { 51 | logrusLogger.Errorf("Error getting token: %v", err) 52 | http.Error(w, "Invalid token", http.StatusBadRequest) 53 | return 54 | } 55 | 56 | client := oauth2Config.Client(context.Background(), token) 57 | 58 | user, err := getUserInfo(client) 59 | if err != nil { 60 | logrusLogger.Errorf("Error getting user info: %v", err) 61 | http.Error(w, "Invalid user info", http.StatusBadRequest) 62 | return 63 | } 64 | 65 | logrusLogger.Debugf("User info: %+v", user) 66 | } 67 | 68 | // Fetch LinkedIn user profile 69 | func getUserInfo(client *http.Client) (map[string]interface{}, error) { 70 | 71 | url := "https://api.linkedin.com/v2/me" 72 | resp, err := client.Get(url) 73 | if err != nil { 74 | logrusLogger.Println("Failed to get user info:", err) 75 | return nil, fmt.Errorf("failed to get user info %w \n ", err) 76 | } 77 | defer resp.Body.Close() 78 | 79 | var userData map[string]interface{} 80 | if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { 81 | logrusLogger.Println("Failed to decode response:", err) 82 | return nil, fmt.Errorf("failed to decode response %w \n ", err) 83 | } 84 | 85 | return userData, nil 86 | 87 | } 88 | -------------------------------------------------------------------------------- /backend/internal/services/student_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | type StudentRepository interface { 4 | InsertOrUpdateStudent(name, email, phone string) error 5 | } 6 | 7 | type StudentService struct { 8 | repo StudentRepository 9 | } 10 | 11 | func NewStudentService(repo StudentRepository) *StudentService { 12 | return &StudentService{ 13 | repo: repo, 14 | } 15 | } 16 | 17 | func (studentService *StudentService) InsertOrUpdateStudent(name, email, phone string) error { 18 | return studentService.repo.InsertOrUpdateStudent(name, email, phone) 19 | } 20 | -------------------------------------------------------------------------------- /backend/internal/services/token_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "faladev/internal/auth" 6 | "fmt" 7 | 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | type TokenRepository interface { 12 | CreateToken(token *oauth2.Token) error 13 | GetToken() (*oauth2.Token, error) 14 | } 15 | 16 | type TokenService struct { 17 | repo TokenRepository 18 | } 19 | 20 | func NewTokenService(repo TokenRepository) *TokenService { 21 | return &TokenService{ 22 | repo: repo, 23 | } 24 | } 25 | 26 | func (tokenService *TokenService) CreateToken(token *oauth2.Token) error { 27 | return tokenService.repo.CreateToken(token) 28 | } 29 | 30 | func (tokenService *TokenService) GetToken() (*oauth2.Token, error) { 31 | return tokenService.repo.GetToken() 32 | } 33 | 34 | func (tokenService *TokenService) ValidateOrRefreshToken(ctx context.Context, config *oauth2.Config) (*oauth2.Token, error) { 35 | 36 | token, err := tokenService.GetToken() 37 | 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to load token: %w", err) 40 | } 41 | 42 | if !token.Valid() { 43 | 44 | token, err = auth.RefreshToken(ctx, config, token) 45 | 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to refresh token: %w", err) 48 | } 49 | 50 | if err := tokenService.CreateToken(token); err != nil { 51 | return nil, fmt.Errorf("failed to save token: %w", err) 52 | } 53 | } 54 | 55 | return token, nil 56 | } 57 | -------------------------------------------------------------------------------- /backend/makefile: -------------------------------------------------------------------------------- 1 | GOCMD=go 2 | GOTEST=$(GOCMD) test 3 | GOCLEAN=$(GOCMD) clean 4 | GOMOD=$(GOCMD) mod 5 | GOLINT_CMD=golangci-lint 6 | GOLINT_BASE_RUN=$(GOLINT_CMD) run 7 | GOLINT_RUN=$(GOLINT_BASE_RUN) 8 | BINARY_DIR=bin 9 | DEADCODE_RUN=deadcode ./... 10 | 11 | .PHONY: deps 12 | deps: 13 | $(GOMOD) tidy 14 | $(GOMOD) vendor 15 | .PHONY: lint 16 | lint: 17 | make -f tools/Makefile install-golangci 18 | $(GOLINT_RUN) 19 | 20 | .PHONY: lint-fix 21 | lint-fix: 22 | $(GOLINT_RUN) --fix 23 | .PHONY: clean 24 | clean: 25 | rm -rf $(BINARY_DIR) 26 | $(GOCLEAN) -cache -modcache # optional 27 | .PHONY: tools 28 | tools: 29 | make -f tools/Makefile 30 | .PHONY: format 31 | format: 32 | make -f tools/Makefile install-gofumpt 33 | gofumpt -l -w . 34 | 35 | .PHONY: mock 36 | mock: 37 | make -f tools/Makefile install-mockery 38 | go generate 39 | .PHONY: deadcode 40 | deadcode: 41 | make -f tools/Makefile install-deadcode 42 | $(DEADCODE_RUN) -------------------------------------------------------------------------------- /backend/pkg/utils/encoding.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | ) 6 | 7 | func Base64URLEncode(src []byte) string { 8 | return base64.URLEncoding.EncodeToString(src) 9 | } 10 | -------------------------------------------------------------------------------- /backend/static/imgs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/backend/static/imgs/.DS_Store -------------------------------------------------------------------------------- /backend/static/imgs/faladev.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/backend/static/imgs/faladev.ico -------------------------------------------------------------------------------- /backend/static/imgs/faladev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/backend/static/imgs/faladev.jpg -------------------------------------------------------------------------------- /backend/static/imgs/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/static/imgs/whatsapp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/static/imgs/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/templates/email/mentorship.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ .Name }} 5 | 6 | 7 |

Faaala Dev!

8 |

Este é um convite para a reunião: {{ .Name }}

9 |

Uma mentoria gratuita na qual nos encontraremos semanalmente para que eu possa te auxiliar, seja você alguém que está entrando na área de tecnologia, migrando de carreira ou buscando crescimento profissional.

10 |

O que esperar?

11 |

Essas sessões são conduzidas de modo a proporcionar orientações práticas e insights valiosos, baseados em meus 15 anos de experiência na área de desenvolvimento e nas contribuições dos demais membros. Será uma ótima oportunidade para:

12 | 17 |

Detalhes da Mentoria:

18 | 25 |

Se você tiver alguma dúvida ou precisar de mais informações, fique à vontade para responder a este e-mail.

26 |

Até lá!

27 |

Atenciosamente,

28 |

{{ .Organizer }}

29 | 30 | 31 | -------------------------------------------------------------------------------- /backend/templates/web/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Mentoria FalaDev - Carreira e Tecnologia 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 | 25 | 26 |
27 |
28 | 31 | 32 |
33 |
34 | 37 | 38 |
39 |
40 | 43 |
44 |
45 |
46 | 60 |

61 | Essa é uma mentoria gratuita para quem está entrando na área de tecnologia, migrando de área ou buscando crescimento profissional. 62 |

63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /backend/tests/google_calendar_service_mocks.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "faladev/internal/services" 6 | 7 | "google.golang.org/api/calendar/v3" 8 | ) 9 | 10 | type FakeCalendarService struct { 11 | EventsListMock func(calendarID string) services.EventsListCall 12 | GetEventMock func(calendarID, eventID string) services.EventCall 13 | UpdateEventMock func(calendarID, eventID string, event *calendar.Event) services.EventCall 14 | InsertEventMock func(calendarID string, event *calendar.Event) services.EventCall 15 | } 16 | 17 | func (f *FakeCalendarService) EventsList(calendarID string) services.EventsListCall { 18 | if f.EventsListMock != nil { 19 | return f.EventsListMock(calendarID) 20 | } 21 | return nil 22 | } 23 | 24 | func (f *FakeCalendarService) GetEvent(calendarID, eventID string) services.EventCall { 25 | if f.GetEventMock != nil { 26 | return f.GetEventMock(calendarID, eventID) 27 | } 28 | return nil 29 | } 30 | 31 | func (f *FakeCalendarService) UpdateEvent(calendarID, eventID string, event *calendar.Event) services.EventCall { 32 | if f.UpdateEventMock != nil { 33 | return f.UpdateEventMock(calendarID, eventID, event) 34 | } 35 | return nil 36 | } 37 | 38 | func (f *FakeCalendarService) InsertEvent(calendarID string, event *calendar.Event) services.EventCall { 39 | if f.InsertEventMock != nil { 40 | return f.InsertEventMock(calendarID, event) 41 | } 42 | return nil 43 | } 44 | 45 | type FakeEventsListCall struct { 46 | DoFunc func() (*calendar.Events, error) 47 | } 48 | 49 | func (f *FakeEventsListCall) Do() (*calendar.Events, error) { 50 | if f.DoFunc != nil { 51 | return f.DoFunc() 52 | } 53 | return nil, errors.New("Do function not implemented") 54 | } 55 | 56 | type FakeEventCall struct { 57 | DoFunc func() (*calendar.Event, error) 58 | } 59 | 60 | func (f *FakeEventCall) Do() (*calendar.Event, error) { 61 | if f.DoFunc != nil { 62 | return f.DoFunc() 63 | } 64 | return nil, errors.New("Do function not implemented") 65 | } 66 | -------------------------------------------------------------------------------- /backend/tests/google_calendar_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "faladev/internal/services" 6 | "testing" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/stretchr/testify/assert" 10 | "google.golang.org/api/calendar/v3" 11 | ) 12 | 13 | func TestFindEventByKey(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | eventKey string 17 | mockDoFunc func() (*calendar.Event, error) 18 | expectedEvent *calendar.Event 19 | expectedError string 20 | }{ 21 | { 22 | name: "Event Found", 23 | eventKey: "04mti3liihmd9u2agf8hg7kf6u", 24 | mockDoFunc: func() (*calendar.Event, error) { 25 | return &calendar.Event{Id: "04mti3liihmd9u2agf8hg7kf6u"}, nil 26 | }, 27 | expectedEvent: &calendar.Event{Id: "04mti3liihmd9u2agf8hg7kf6u"}, 28 | expectedError: "", 29 | }, 30 | { 31 | name: "Event Not Found", 32 | eventKey: "https://meet.google.com/non-existent", 33 | mockDoFunc: func() (*calendar.Event, error) { 34 | return nil, errors.New("event not found") 35 | }, 36 | expectedEvent: nil, 37 | expectedError: "error fetching event with eventKey https://meet.google.com/non-existent: event not found", 38 | }, 39 | { 40 | name: "API Error", 41 | eventKey: "04mti3liihmd9u2agf8hg7kf6u", 42 | mockDoFunc: func() (*calendar.Event, error) { 43 | return nil, errors.New("API error") 44 | }, 45 | expectedEvent: nil, 46 | expectedError: "error fetching event with eventKey 04mti3liihmd9u2agf8hg7kf6u: API error", 47 | }, 48 | } 49 | 50 | for _, tt := range tests { 51 | 52 | t.Run(tt.name, func(t *testing.T) { 53 | 54 | mockService := &FakeCalendarService{ 55 | GetEventMock: func(calendarID, eventID string) services.EventCall { 56 | assert.Equal(t, "primary", calendarID) 57 | assert.Equal(t, tt.eventKey, eventID) 58 | return &FakeEventCall{ 59 | DoFunc: tt.mockDoFunc, 60 | } 61 | }, 62 | } 63 | 64 | googleCalendarService := services.GoogleCalendarService{} 65 | 66 | event, err := googleCalendarService.FindEventByKey(context.Background(), mockService, tt.eventKey) 67 | 68 | if tt.expectedError != "" { 69 | assert.EqualError(t, err, tt.expectedError) 70 | assert.Error(t, err) 71 | assert.Nil(t, event) 72 | } else { 73 | assert.NoError(t, err) 74 | assert.Equal(t, tt.expectedEvent, event) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/tests/linkedIn_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type MockLinkedInService struct { 10 | LoginFunc func() string 11 | CallbackFunc func(state string, code string) (map[string]interface{}, error) 12 | GetUserInfoFunc func(token string) (map[string]interface{}, error) 13 | } 14 | 15 | // Login method simulates redirecting to LinkedIn for login 16 | func (m *MockLinkedInService) Login() string { 17 | if m.LoginFunc != nil { 18 | return m.LoginFunc() 19 | } 20 | return "mock-login-url" 21 | } 22 | 23 | // Callback method simulates handling the callback from LinkedIn 24 | func (m *MockLinkedInService) Callback(state string, code string) (map[string]interface{}, error) { 25 | if m.CallbackFunc != nil { 26 | return m.CallbackFunc(state, code) 27 | } 28 | return nil, fmt.Errorf("mock callback error") 29 | } 30 | 31 | // GetUserInfo method simulates fetching user information 32 | func (m *MockLinkedInService) GetUserInfo(token string) (map[string]interface{}, error) { 33 | if m.GetUserInfoFunc != nil { 34 | return m.GetUserInfoFunc(token) 35 | } 36 | return nil, fmt.Errorf("mock user info error") 37 | } 38 | 39 | // Example of using the mock service 40 | func ExampleUsage() { 41 | mockService := &MockLinkedInService{ 42 | LoginFunc: func() string { 43 | return "https://mock.linkedin.com/auth?code=123&state=abc123" 44 | }, 45 | CallbackFunc: func(state string, code string) (map[string]interface{}, error) { 46 | if state != "abc123" { 47 | return nil, fmt.Errorf("invalid state") 48 | } 49 | return map[string]interface{}{ 50 | "firstName": "John", 51 | "lastName": "Doe", 52 | "email": "john.doe@example.com", 53 | }, nil 54 | }, 55 | GetUserInfoFunc: func(token string) (map[string]interface{}, error) { 56 | return map[string]interface{}{ 57 | "id": "12345", 58 | "firstName": "John", 59 | "lastName": "Doe", 60 | "email": "john.doe@example.com", 61 | }, nil 62 | }, 63 | } 64 | 65 | // Simulating login 66 | loginURL := mockService.Login() 67 | logrus.Infof("Login URL: %s", loginURL) 68 | 69 | // Simulating callback handling 70 | userInfo, err := mockService.Callback("abc123", "mock-code") 71 | if err != nil { 72 | logrus.Errorf("Error in callback: %v", err) 73 | return 74 | } 75 | logrus.Infof("User info: %+v", userInfo) 76 | 77 | // Simulating fetching user info 78 | user, err := mockService.GetUserInfo("mock-token") 79 | if err != nil { 80 | logrus.Errorf("Error getting user info: %v", err) 81 | return 82 | } 83 | logrus.Infof("Fetched user: %+v", user) 84 | } 85 | -------------------------------------------------------------------------------- /backend/tools/Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=go 2 | GOINSTALL=$(GOCMD) install 3 | CURL=curl 4 | 5 | .PHONY: all 6 | all: install 7 | .PHONY: install 8 | install: install-swagger install-golangci install-gofumpt install-mockery 9 | .PHONY: install-swagger 10 | install-swagger: 11 | $(GOINSTALL) github.com/swaggo/swag/cmd/swag@v1.16.3 12 | .PHONY: install-golangci 13 | install-golangci: 14 | $(CURL) -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOENVPATH) v1.61.0 15 | .PHONY: install-gofumpt 16 | install-gofumpt: 17 | $(GOINSTALL) mvdan.cc/gofumpt@latest 18 | .PHONY: install-mockery 19 | install-mockery: 20 | $(GOINSTALL) github.com/vektra/mockery/v2@v2.30.1 21 | .PHONY: install-deadcode 22 | install-deadcode: 23 | $(GOINSTALL) golang.org/x/tools/cmd/deadcode@latest -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:13 4 | container_name: postgres 5 | environment: 6 | POSTGRES_USER: userpostgres 7 | POSTGRES_PASSWORD: passwordpostgres 8 | POSTGRES_DB: faladev 9 | ports: 10 | - '5432:5432' 11 | volumes: 12 | - postgres-data:/var/lib/postgresql/data 13 | 14 | pgadmin: 15 | image: dpage/pgadmin4 16 | container_name: pgadmin 17 | environment: 18 | PGADMIN_DEFAULT_EMAIL: admin@admin.com 19 | PGADMIN_DEFAULT_PASSWORD: admin 20 | ports: 21 | - '5050:80' 22 | volumes: 23 | - ./servers.json:/pgadmin4/servers.json 24 | 25 | jaeger: 26 | image: jaegertracing/all-in-one:1.31 27 | container_name: jaeger 28 | ports: 29 | - '5775:5775/udp' 30 | - '6831:6831/udp' 31 | - '6832:6832/udp' 32 | - '5778:5778' 33 | - '16686:16686' 34 | - '14268:14268' 35 | - '14250:14250' 36 | - '9411:9411' 37 | 38 | backend: 39 | build: 40 | context: . 41 | dockerfile: ./Dockerfile 42 | container_name: faladev 43 | environment: 44 | DB_HOST: postgres 45 | DB_PORT: 5432 46 | DB_USER: userpostgres 47 | DB_PASSWORD: passwordpostgres 48 | DB_NAME: faladev 49 | DATABASE_URL: postgres://userpostgres:passwordpostgres@postgres:5432/faladev?sslmode=disable 50 | OTEL_EXPORTER_JAEGER_ENDPOINT: http://jaeger:14268/api/traces 51 | ports: 52 | - '8080:8080' 53 | volumes: 54 | - ./backend:/app/backend 55 | depends_on: 56 | - postgres 57 | - jaeger 58 | 59 | frontend: 60 | image: node:20-alpine 61 | container_name: frontend 62 | working_dir: /app 63 | command: sh -c "npm install && npm run dev" 64 | volumes: 65 | - ./frontend:/app 66 | - /app/node_modules 67 | ports: 68 | - '3000:3000' 69 | networks: 70 | - app-network 71 | 72 | networks: 73 | app-network: 74 | driver: bridge 75 | 76 | volumes: 77 | postgres-data: 78 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Running database migrations..." 6 | 7 | cd backend 8 | 9 | go run cmd/migrate/main.go 10 | 11 | echo "Starting the application with reflex..." 12 | 13 | exec reflex -r '\.go$' -s -- sh -c "go run cmd/api/main.go" -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "plugin:storybook/recommended"], 3 | "rules": { 4 | "@typescript-eslint/no-misused-promises": "off", 5 | "@typescript-eslint/consistent-type-assertions": "off", 6 | "import/order": [ 7 | "warn", 8 | { 9 | "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], 10 | "newlines-between": "always", 11 | "alphabetize": { "order": "asc", "caseInsensitive": true } 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | *storybook.log -------------------------------------------------------------------------------- /frontend/.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "disown-opener": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /frontend/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | stages: [commit] 7 | args: ['--no-fail-on-changed'] # Não falha, mesmo que haja espaços em branco 8 | - id: end-of-file-fixer 9 | stages: [commit] 10 | args: ['--no-fail-on-changed'] # Não falha, mesmo que haja fim de linha incorreto 11 | - id: check-yaml 12 | stages: [commit] 13 | args: ['--no-fail-on-changed'] # Não falha mesmo que o YAML tenha erros 14 | - id: trailing-whitespace 15 | stages: [post-commit] 16 | args: ['--no-fail-on-changed'] # Permite passar após commit sem erros 17 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /frontend/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs' 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-onboarding', 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@chromatic-com/storybook', 10 | '@storybook/addon-interactions', 11 | ], 12 | framework: { 13 | name: '@storybook/nextjs', 14 | options: {}, 15 | }, 16 | docs: { 17 | autodocs: true, 18 | }, 19 | staticDirs: ['../public'], 20 | } 21 | export default config 22 | -------------------------------------------------------------------------------- /frontend/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react' 2 | import 'tailwindcss/tailwind.css' 3 | import '../src/app/globals.css' 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i, 11 | }, 12 | }, 13 | nextjs: { 14 | appDirectory: true, 15 | }, 16 | layout: 'fullscreen', 17 | 18 | backgrounds: { 19 | default: 'white', 20 | }, 21 | }, 22 | } 23 | 24 | export default preview 25 | -------------------------------------------------------------------------------- /frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /frontend/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const afterAll: typeof import('vitest')['afterAll'] 9 | const afterEach: typeof import('vitest')['afterEach'] 10 | const assert: typeof import('vitest')['assert'] 11 | const beforeAll: typeof import('vitest')['beforeAll'] 12 | const beforeEach: typeof import('vitest')['beforeEach'] 13 | const chai: typeof import('vitest')['chai'] 14 | const describe: typeof import('vitest')['describe'] 15 | const expect: typeof import('vitest')['expect'] 16 | const it: typeof import('vitest')['it'] 17 | const suite: typeof import('vitest')['suite'] 18 | const test: typeof import('vitest')['test'] 19 | const vi: typeof import('vitest')['vi'] 20 | const vitest: typeof import('vitest')['vitest'] 21 | } 22 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /frontend/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) {}, 6 | baseUrl: 'http://localhost:3000', 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/Mentoring/MentotingPage.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | 3 | export class MentoringPage { 4 | static visit() { 5 | cy.visit('/') 6 | } 7 | 8 | static clickSubmitButton() { 9 | cy.get('[data-testid="button-submit"]').click() 10 | } 11 | 12 | static fillEmailInput() { 13 | cy.get('#email').type(faker.internet.email()) 14 | } 15 | 16 | static fillNameInput() { 17 | cy.get('#name').type(faker.internet.userName()) 18 | } 19 | 20 | static fillPhoneInput() { 21 | cy.get('#phone').type('11960606060') 22 | } 23 | 24 | static getDisplayedErrors(expectedCount: number) { 25 | cy.get('[data-testid="error-message"]').should('have.length', expectedCount) 26 | } 27 | 28 | static verificarMensagemDeAlerta(title: string, description: string) { 29 | cy.get('[data-testid="alert-title"]').should('have.text', title) 30 | cy.get('[data-testid="alert-description"]').should('have.text', description) 31 | } 32 | 33 | static MockRegisterSuccess() { 34 | cy.intercept('POST', 'http://localhost:8080/api/events ', req => { 35 | req.reply({ 36 | statusCode: 200, 37 | body: { response: 'success' }, 38 | }) 39 | }).as('requestRegisterMentoring') 40 | } 41 | 42 | static MockRegisterFail() { 43 | cy.intercept('POST', 'http://localhost:8080/api/events ', req => { 44 | req.reply({ 45 | statusCode: 400, 46 | body: { response: 'success' }, 47 | }) 48 | }).as('requestRegisterMentoring') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/Mentoring/mentoring.spec.cy.ts: -------------------------------------------------------------------------------- 1 | import { registrationStatusMessages } from '../../../src/shared/registrationStatusMessages' 2 | 3 | import { MentoringPage } from './MentotingPage' 4 | 5 | describe('', () => { 6 | it('should display errors on the screen after form submission', () => { 7 | MentoringPage.visit() 8 | MentoringPage.clickSubmitButton() 9 | MentoringPage.getDisplayedErrors(3) 10 | 11 | MentoringPage.fillEmailInput() 12 | MentoringPage.getDisplayedErrors(2) 13 | 14 | MentoringPage.fillNameInput() 15 | MentoringPage.getDisplayedErrors(1) 16 | 17 | MentoringPage.fillPhoneInput() 18 | MentoringPage.getDisplayedErrors(0) 19 | }) 20 | it('Should Display Success Message After Successful Registration', () => { 21 | MentoringPage.MockRegisterSuccess() 22 | MentoringPage.visit() 23 | 24 | MentoringPage.fillEmailInput() 25 | MentoringPage.fillNameInput() 26 | MentoringPage.fillPhoneInput() 27 | 28 | MentoringPage.clickSubmitButton() 29 | MentoringPage.verificarMensagemDeAlerta( 30 | registrationStatusMessages.success.title, 31 | registrationStatusMessages.success.description, 32 | ) 33 | }) 34 | 35 | it('Should Display Success Message After Successful Registration', () => { 36 | MentoringPage.MockRegisterFail() 37 | MentoringPage.visit() 38 | 39 | MentoringPage.fillEmailInput() 40 | MentoringPage.fillNameInput() 41 | MentoringPage.fillPhoneInput() 42 | 43 | MentoringPage.clickSubmitButton() 44 | MentoringPage.verificarMensagemDeAlerta( 45 | registrationStatusMessages.error.title, 46 | registrationStatusMessages.error.description, 47 | ) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/coverage/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | if ( 67 | document.getElementById('fileSearch') === document.activeElement && 68 | document.activeElement != null 69 | ) { 70 | // if we're currently focused on the search input, we don't want to navigate 71 | return; 72 | } 73 | 74 | switch (event.which) { 75 | case 78: // n 76 | case 74: // j 77 | goToNext(); 78 | break; 79 | case 66: // b 80 | case 75: // k 81 | case 80: // p 82 | goToPrevious(); 83 | break; 84 | } 85 | }; 86 | })(); 87 | window.addEventListener('keydown', jumpToCode); 88 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/coverage/coverage-final.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/coverage/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/frontend/cypress/e2e/coverage/favicon.png -------------------------------------------------------------------------------- /frontend/cypress/e2e/coverage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for All files 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files

23 |
24 | 25 |
26 | Unknown% 27 | Statements 28 | 0/0 29 |
30 | 31 | 32 |
33 | Unknown% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | Unknown% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | Unknown% 48 | Lines 49 | 0/0 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
FileStatementsBranchesFunctionsLines
83 |
84 |
85 |
86 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/coverage/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/coverage/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/frontend/cypress/e2e/coverage/sort-arrow-sprite.png -------------------------------------------------------------------------------- /frontend/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /frontend/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts 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') -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | export default nextConfig 5 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "storybook": "storybook dev -p 6006", 11 | "build-storybook": "storybook build", 12 | "test": "vitest", 13 | "coverage": "vitest --coverage", 14 | "cypress": "cypress open", 15 | "test:cypress:run": "cypress run" 16 | }, 17 | "dependencies": { 18 | "@faker-js/faker": "^9.0.0", 19 | "@hookform/resolvers": "^3.9.0", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-slot": "^1.1.0", 22 | "@tanstack/react-query": "^5.52.0", 23 | "axios": "^1.7.2", 24 | "class-variance-authority": "^0.7.0", 25 | "clsx": "^2.1.1", 26 | "lucide-react": "^0.417.0", 27 | "next": "14.2.5", 28 | "react": "^18", 29 | "react-dom": "^18", 30 | "react-dropzone": "^14.2.3", 31 | "react-hook-form": "^7.53.0", 32 | "react-icons": "^5.2.1", 33 | "tailwind-merge": "^2.4.0", 34 | "tailwindcss-animate": "^1.0.7", 35 | "unplugin-auto-import": "^0.18.2", 36 | "vite-tsconfig-paths": "^5.0.1", 37 | "zod": "^3.23.8" 38 | }, 39 | "devDependencies": { 40 | "@chromatic-com/storybook": "^1.6.1", 41 | "@storybook/addon-essentials": "^8.2.5", 42 | "@storybook/addon-interactions": "^8.2.5", 43 | "@storybook/addon-links": "^8.2.5", 44 | "@storybook/addon-onboarding": "^8.2.5", 45 | "@storybook/blocks": "^8.2.5", 46 | "@storybook/nextjs": "^8.2.5", 47 | "@storybook/react": "^8.2.5", 48 | "@storybook/test": "^8.2.5", 49 | "@testing-library/dom": "^10.4.0", 50 | "@testing-library/react": "^16.0.0", 51 | "@testing-library/user-event": "^14.5.2", 52 | "@types/jest": "^29.5.12", 53 | "@types/node": "^20.14.13", 54 | "@types/react": "^18", 55 | "@types/react-dom": "^18", 56 | "@types/react-dropzone": "^4.2.2", 57 | "@vitejs/plugin-react": "^4.3.1", 58 | "@vitest/coverage-v8": "^2.0.5", 59 | "@vitest/ui": "^2.0.5", 60 | "cypress": "^13.14.2", 61 | "eslint": "^8", 62 | "eslint-config-next": "14.2.5", 63 | "eslint-plugin-storybook": "^0.8.0", 64 | "jsdom": "^24.1.1", 65 | "postcss": "^8", 66 | "storybook": "^8.2.5", 67 | "tailwindcss": "^3.4.1", 68 | "typescript": "^5", 69 | "vitest": "^2.0.4" 70 | }, 71 | "engines": { 72 | "node": ">=18.17.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /frontend/public/assets/person-unknow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/frontend/public/assets/person-unknow.png -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/imgs/faladev.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/frontend/public/static/imgs/faladev.ico -------------------------------------------------------------------------------- /frontend/public/static/imgs/faladev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dedevpradev/faladev/462018a1293d782d7902dc835bf7fc3f6e039f95/frontend/public/static/imgs/faladev.jpg -------------------------------------------------------------------------------- /frontend/public/static/imgs/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/public/static/imgs/whatsapp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/imgs/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/Mutate/useMutationMentoring.ts: -------------------------------------------------------------------------------- 1 | import { MutationOptions, useMutation } from '@tanstack/react-query' 2 | 3 | import { SchemaMentoringType } from '@/app/(mentoring)/mentoring.type' 4 | import { IMentoringAgendaService } from '@/services/MentoringAgenda/MentoringAgenda.service' 5 | 6 | type MutationMentoringProps = { 7 | service: IMentoringAgendaService 8 | } & Omit, 'mutationFn'> 9 | 10 | export const useMutationMentoring = ({ 11 | service, 12 | ...mutationMentoringProps 13 | }: MutationMentoringProps) => { 14 | return useMutation({ 15 | mutationFn: data => service.SignUpMentoring(data), 16 | ...mutationMentoringProps, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/Mutate/useUserMutation.ts: -------------------------------------------------------------------------------- 1 | import { MutationOptions, useMutation } from '@tanstack/react-query' 2 | import { IUserService, UserRegisterData } from '@/services/User/User.service' 3 | 4 | type MutationProps = { 5 | service: IUserService 6 | } & Omit, 'mutationFn'> 7 | 8 | // TODO: abstrair para useMudation -> usar generics para desacoplar hook do domínio 9 | export const useMutationUser = ({ 10 | service, 11 | ...mutationProps 12 | }: MutationProps) => { 13 | return useMutation({ 14 | mutationFn: data => service.RegisterUser(data), 15 | ...mutationProps, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/Provider/ReactQueryProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 4 | import React from 'react' 5 | 6 | const queryClient = new QueryClient() 7 | 8 | export const ReactQueryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 9 | return {children} 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/mentoring.model.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | import { renderWithQueryClient } from '@/tests/renderWithQueryClient' 3 | import { waitFor } from '@testing-library/react' 4 | import { expect } from 'vitest' 5 | import { useMentoringModel } from './mentoring.model' 6 | import { 7 | failedMentoringServiceMock, 8 | successfulMentoringServiceMock, 9 | } from '@/tests/mock/mentoringServiceMock' 10 | import { mockSchemaMentoringTypeData } from '@/tests/mock/mockSchemaMentoringTypeData' 11 | import { registrationStatusMessages } from '../../shared/registrationStatusMessages' 12 | 13 | describe('useMentoringModel', () => { 14 | it('should return initial state', () => { 15 | const { result } = renderWithQueryClient(() => 16 | useMentoringModel(successfulMentoringServiceMock), 17 | ) 18 | expect(result.current.registrationResult).toBeNull() 19 | expect(result.current.errors).toEqual({}) 20 | expect(result.current.isSubmitting).toBe(false) 21 | }) 22 | 23 | it('should set registrationResult to success on successful submission', async () => { 24 | const { result } = renderWithQueryClient(() => 25 | useMentoringModel(successfulMentoringServiceMock), 26 | ) 27 | result.current.handleSubmitMentoring(mockSchemaMentoringTypeData) 28 | await waitFor(() => { 29 | expect(result.current.registrationResult).toEqual(registrationStatusMessages.success) 30 | }) 31 | }) 32 | 33 | it('should set registrationResult to error on failed submission', async () => { 34 | const { result } = renderWithQueryClient(() => useMentoringModel(failedMentoringServiceMock)) 35 | result.current.handleSubmitMentoring(mockSchemaMentoringTypeData) 36 | await waitFor(() => { 37 | expect(result.current.registrationResult).toEqual(registrationStatusMessages.error) 38 | }) 39 | }) 40 | 41 | it('should return errors from useForm', () => { 42 | const { result } = renderWithQueryClient(() => 43 | useMentoringModel(successfulMentoringServiceMock), 44 | ) 45 | const errors = {} 46 | expect(result.current.errors).toEqual(errors) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/mentoring.model.ts: -------------------------------------------------------------------------------- 1 | import { zodResolver } from '@hookform/resolvers/zod' 2 | import { useState } from 'react' 3 | import { useForm } from 'react-hook-form' 4 | 5 | import { useMutationMentoring } from '@/Mutate/useMutationMentoring' 6 | import { IMentoringAgendaService } from '@/services/MentoringAgenda/MentoringAgenda.service' 7 | 8 | import { SchemaMentoring } from './mentoring.schema' 9 | import { RegistrationResult, SchemaMentoringType } from './mentoring.type' 10 | import { registrationStatusMessages } from '../../shared/registrationStatusMessages' 11 | 12 | export function useMentoringModel(mentoringService: IMentoringAgendaService) { 13 | const [registrationResult, setRegistrationResult] = useState(null) 14 | const onRegistrationSuccess = () => setRegistrationResult(registrationStatusMessages.success) 15 | const onRegistrationError = () => setRegistrationResult(registrationStatusMessages.error) 16 | const handleSubmitMentoring = (data: SchemaMentoringType) => createMentoringAgenda(data) 17 | 18 | const { 19 | register, 20 | handleSubmit, 21 | formState: { errors, isSubmitting }, 22 | } = useForm({ 23 | resolver: zodResolver(SchemaMentoring), 24 | }) 25 | const submitButtonLabel = isSubmitting ? 'Registrando...' : 'Quero participar' 26 | 27 | const { mutate: createMentoringAgenda } = useMutationMentoring({ 28 | service: mentoringService, 29 | onError: onRegistrationError, 30 | onSuccess: onRegistrationSuccess, 31 | }) 32 | 33 | return { 34 | register, 35 | handleSubmit, 36 | handleSubmitMentoring, 37 | errors, 38 | registrationResult, 39 | isSubmitting, 40 | submitButtonLabel, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/mentoring.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const SchemaMentoring = z.object({ 4 | name: z 5 | .string() 6 | .min(3, { message: 'Nome deve ter no mínimo 3 caracteres' }) 7 | .max(50, { message: 'Nome deve ter no máximo 50 caracteres' }) 8 | .trim(), 9 | email: z.string().email({ message: 'Endereço de email inválido' }).trim(), 10 | phone: z 11 | .string({ required_error: 'Telefone é necessário' }) 12 | .min(10, { message: 'Telefone deve ter no mínimo 10 dígitos' }) 13 | .max(15, { message: 'Telefone deve ter no máximo 15 dígitos' }) 14 | .regex(/^\d+$/, { message: 'Telefone deve conter apenas números' }) 15 | .trim(), 16 | }) 17 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/mentoring.type.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { SchemaMentoring } from './mentoring.schema' 4 | 5 | export type Status = 'error' | 'success' 6 | 7 | export type RegistrationResult = { 8 | title: string 9 | description: string 10 | status: Status 11 | } 12 | 13 | export type SchemaMentoringType = z.infer 14 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/mentoring.view.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { useMentoringModel } from './mentoring.model' 4 | 5 | import { TextInput } from '@/components/form/text-input' 6 | import { AlertBox } from '@/components/ui/alert-box' 7 | import { ErrorMessage } from '@/components/ui/error-message' 8 | 9 | 10 | type MentoringViewProps = ReturnType 11 | 12 | export function MentoringView(props: MentoringViewProps) { 13 | const { 14 | register, 15 | handleSubmitMentoring, 16 | handleSubmit, 17 | errors, 18 | registrationResult, 19 | isSubmitting, 20 | submitButtonLabel, 21 | } = props 22 | return ( 23 |
24 |
25 |
30 |
31 | 38 | } name="name" /> 39 |
40 |
41 | 49 | } name="email" /> 50 |
51 |
52 | 59 | } name="phone" /> 60 |
61 |
62 | 72 |
73 |
74 | {registrationResult && ( 75 | 81 | )} 82 |
83 |

84 | ou 85 | 86 | cadastre-se 87 | 88 | na nossa plataforma! 89 |

90 |
91 |
92 | 93 |

94 | Essa é uma mentoria gratuita para quem está entrando na área de tecnologia, migrando de 95 | área ou buscando crescimento profissional. 96 |

97 |
98 |
99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, RenderResult, waitFor } from '@testing-library/react' 2 | import { describe, expect } from 'vitest' 3 | 4 | import { IMentoringAgendaService } from '@/services/MentoringAgenda/MentoringAgenda.service' 5 | import { changeInput } from '@/tests/changeInput' 6 | import { 7 | failedMentoringServiceMock, 8 | successfulMentoringServiceMock, 9 | } from '@/tests/mock/mentoringServiceMock' 10 | import { mockSchemaMentoringTypeData } from '@/tests/mock/mockSchemaMentoringTypeData' 11 | import { renderView } from '@/tests/renderView' 12 | 13 | import { useMentoringModel } from './mentoring.model' 14 | import { MentoringView } from './mentoring.view' 15 | import { registrationStatusMessages } from '../../shared/registrationStatusMessages' 16 | 17 | type MakeSutProps = { 18 | service?: IMentoringAgendaService 19 | } 20 | 21 | const MakeSut = ({ service = successfulMentoringServiceMock }: MakeSutProps) => { 22 | const methods = useMentoringModel(service) 23 | return 24 | } 25 | 26 | type SubmitFormParams = { 27 | screen: RenderResult 28 | title: string 29 | description: string 30 | } 31 | 32 | const SubmitForm = async (params: SubmitFormParams) => { 33 | const { description, screen, title } = params 34 | 35 | const emailInput = screen.container.querySelector('#email') as HTMLInputElement 36 | changeInput({ input: emailInput, valueInput: mockSchemaMentoringTypeData.email }) 37 | 38 | const nameInput = screen.container.querySelector('#name') as HTMLInputElement 39 | changeInput({ input: nameInput, valueInput: mockSchemaMentoringTypeData.name }) 40 | 41 | const phoneInput = screen.container.querySelector('#phone') as HTMLInputElement 42 | changeInput({ input: phoneInput, valueInput: mockSchemaMentoringTypeData.phone }) 43 | 44 | const buttonSubmit = screen.getByTestId('button-submit') 45 | fireEvent.click(buttonSubmit) 46 | 47 | const formSubmit = screen.getByTestId('form-mentoring') 48 | await waitFor(() => formSubmit) 49 | 50 | const alertTitle = screen.getByTestId('alert-title') 51 | expect(alertTitle.textContent).toBe(title) 52 | 53 | const alertDescription = screen.getByTestId('alert-description') 54 | expect(alertDescription.textContent).toBe(description) 55 | } 56 | 57 | describe('', () => { 58 | test('displays all error messages on form submission', async () => { 59 | const screen = renderView() 60 | 61 | const buttonSubmit = screen.getByTestId('button-submit') 62 | fireEvent.click(buttonSubmit) 63 | 64 | const formSubmit = screen.getByTestId('form-mentoring') 65 | await waitFor(() => formSubmit) 66 | 67 | const errorMessage = screen.getAllByTestId('error-message') 68 | expect(errorMessage.length).toBe(3) 69 | }) 70 | test('Should display error messages after invalid email submission', async () => { 71 | const screen = renderView() 72 | 73 | const buttonSubmit = screen.getByTestId('button-submit') 74 | fireEvent.click(buttonSubmit) 75 | 76 | const formSubmit = screen.getByTestId('form-mentoring') 77 | await waitFor(() => formSubmit) 78 | 79 | const emailInput = screen.container.querySelector('#email') as HTMLInputElement 80 | changeInput({ input: emailInput, valueInput: mockSchemaMentoringTypeData.email }) 81 | fireEvent.click(buttonSubmit) 82 | await waitFor(() => formSubmit) 83 | 84 | const errorMessage = screen.getAllByTestId('error-message') 85 | expect(errorMessage.length).toBe(2) 86 | }) 87 | 88 | test('should display a single error message after submitting form with invalid email and name', async () => { 89 | const screen = renderView() 90 | 91 | const buttonSubmit = screen.getByTestId('button-submit') 92 | fireEvent.click(buttonSubmit) 93 | 94 | const formSubmit = screen.getByTestId('form-mentoring') 95 | await waitFor(() => formSubmit) 96 | 97 | const emailInput = screen.container.querySelector('#email') as HTMLInputElement 98 | changeInput({ input: emailInput, valueInput: mockSchemaMentoringTypeData.email }) 99 | 100 | const nameInput = screen.container.querySelector('#name') as HTMLInputElement 101 | changeInput({ input: nameInput, valueInput: mockSchemaMentoringTypeData.name }) 102 | fireEvent.click(buttonSubmit) 103 | await waitFor(() => formSubmit) 104 | 105 | const errorMessage = screen.getAllByTestId('error-message') 106 | expect(errorMessage.length).toBe(1) 107 | }) 108 | 109 | test('should display success alert with correct title and description on successful form submission', async () => { 110 | const screen = renderView() 111 | 112 | await SubmitForm({ 113 | screen, 114 | title: registrationStatusMessages.success.title, 115 | description: registrationStatusMessages.success.description, 116 | }) 117 | }) 118 | 119 | test('displays error messages on failed mentoring submission', async () => { 120 | const screen = renderView() 121 | 122 | await SubmitForm({ 123 | screen, 124 | title: registrationStatusMessages.error.title, 125 | description: registrationStatusMessages.error.description, 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /frontend/src/app/(mentoring)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { HttpAxiosAdapter } from '@/infra/http/HttpClient' 3 | import { MentoringAgendaService } from '@/services/MentoringAgenda/MentoringAgenda.service' 4 | 5 | import { useMentoringModel } from './mentoring.model' 6 | import { MentoringView } from './mentoring.view' 7 | 8 | export default function Home() { 9 | const httpAxiosAdapter = new HttpAxiosAdapter() 10 | const mentoringAgendaService = new MentoringAgendaService(httpAxiosAdapter) 11 | const methods = useMentoringModel(mentoringAgendaService) 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/(users)/register/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { RegisterView } from './register.view' 4 | import { HttpAxiosAdapter } from '@/infra/http/HttpClient' 5 | import { UserService } from '@/services/User/User.service' 6 | import { useUserModel } from './user.model' 7 | 8 | export default function RegisterPage() { 9 | 10 | const httpAxiosAdapter = new HttpAxiosAdapter() 11 | const userService = new UserService(httpAxiosAdapter) 12 | const methods = useUserModel(userService) 13 | 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/(users)/register/register.model.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react" 2 | import { useUserModel } from "./user.model" 3 | import { MockUserService } from "@/tests/mock/userServiceMock" 4 | import { renderWithQueryClient } from "@/tests/renderWithQueryClient" 5 | import { expect } from "vitest" 6 | 7 | describe('useUserModel', () => { 8 | it('should return correct initial state', () => { 9 | const mockService = new MockUserService() 10 | const { result } = renderWithQueryClient(() => useUserModel(mockService)) 11 | 12 | expect(result.current.isSubmitting).equal(false) 13 | expect(result.current.errors).toEqual({}) 14 | expect(result.current.registrationStatus).toBeUndefined() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /frontend/src/app/(users)/register/register.view.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 3 | import { ErrorMessage } from "@/components/ui/error-message" 4 | import { Input } from "@/components/ui/input" 5 | import { Label } from "@/components/ui/label" 6 | import { useUserModel } from "./user.model" 7 | import { AlertBox } from "@/components/ui/alert-box" 8 | 9 | type RegisterViewProps = ReturnType 10 | 11 | export const RegisterView = (props: RegisterViewProps) => { 12 | const { register, errors, isSubmitting, handleUserRegister, registrationStatus } = props 13 | 14 | return ( 15 |
16 | 17 | 18 | Cadastre-se! 19 | Cadastre suas informações para criar uma conta 20 | 21 | 22 |
23 | 24 | 25 | } name="name" /> 26 |
27 |
28 | 29 | 30 | } name="email" /> 31 |
32 |
33 | 34 | 35 | } name="password" /> 36 |
37 |
38 | 39 | 40 | } name="confirmPassword" /> 41 |
42 | 45 | {registrationStatus && ( 46 | 52 | )} 53 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/(users)/register/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { UserSchema } from './user.schema' 4 | 5 | export type Status = 'error' | 'success' 6 | 7 | export type RegistrationResult = { 8 | title: string 9 | description: string 10 | status: Status 11 | } 12 | 13 | export type SchemaUserType = z.infer 14 | -------------------------------------------------------------------------------- /frontend/src/app/(users)/register/user.model.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { IUserService, UserRegisterData, type UserService } from "@/services/User/User.service"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import { UserSchema } from "./user.schema"; 7 | import { registrationStatusMessages } from "@/shared/registrationStatusMessages"; 8 | import { useMutationUser } from "@/Mutate/useUserMutation"; 9 | import { useState } from "react"; 10 | import { RegistrationResult } from "./types"; 11 | 12 | export const useUserModel = (userService: IUserService) => { 13 | const [registrationStatus, setRegistrationStatus] = useState() 14 | 15 | const { 16 | register, 17 | handleSubmit, 18 | formState: { errors, isSubmitting }, 19 | } = useForm({ 20 | resolver: zodResolver(UserSchema), 21 | }) 22 | 23 | const handleUserRegister = handleSubmit((data: UserRegisterData) => registerUser(data)) 24 | 25 | const { mutate: registerUser } = useMutationUser({ 26 | service: userService, 27 | onError: () => setRegistrationStatus(registrationStatusMessages.error), 28 | onSuccess: () => setRegistrationStatus(registrationStatusMessages.success), 29 | }) 30 | 31 | return { 32 | register, 33 | errors, 34 | isSubmitting, 35 | handleUserRegister, 36 | registrationStatus 37 | } 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/src/app/(users)/register/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const UserSchema = z.object({ 4 | name: z 5 | .string() 6 | .min(3, { message: 'Nome deve ter no mínimo 3 caracteres' }) 7 | .max(50, { message: 'Nome deve ter no máximo 50 caracteres' }) 8 | .trim(), 9 | email: z.string().email({ message: 'Endereço de email inválido' }).trim(), 10 | password: z.string().min(8, { message: 'Senha deve ter no mínimo 8 caracteres' }).trim(), 11 | confirmPassword: z.string().min(8, { message: 'Senha deve ter no mínimo 8 caracteres' }).trim() 12 | }) 13 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 0 0% 3.9%; 36 | --foreground: 0 0% 98%; 37 | --card: 0 0% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 0 0% 3.9%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 0 0% 98%; 42 | --primary-foreground: 0 0% 9%; 43 | --secondary: 0 0% 14.9%; 44 | --secondary-foreground: 0 0% 98%; 45 | --muted: 0 0% 14.9%; 46 | --muted-foreground: 0 0% 63.9%; 47 | --accent: 0 0% 14.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 0 0% 98%; 51 | --border: 0 0% 14.9%; 52 | --input: 0 0% 14.9%; 53 | --ring: 0 0% 83.1%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/app/layout.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import RootLayout from '@/app/layout' 5 | 6 | vi.mock('next/font/google', () => ({ 7 | Inter: vi.fn().mockReturnValue({ className: 'mocked-inter-font' }), 8 | })) 9 | 10 | describe('RootLayout', () => { 11 | it('should render the header with the logo', () => { 12 | render( 13 | 14 |
Child Component
15 |
, 16 | ) 17 | 18 | const logo = screen.getByAltText('FalaDev Logo') 19 | expect(logo).toBeTruthy() 20 | }) 21 | 22 | it('should render the children content', () => { 23 | render( 24 | 25 |
Child Component
26 |
, 27 | ) 28 | 29 | const childContent = screen.getByText('Child Component') 30 | expect(childContent).toBeTruthy() 31 | }) 32 | 33 | it('should render the footer with the Instagram link', () => { 34 | render( 35 | 36 |
Child Component
37 |
, 38 | ) 39 | 40 | const instagramLink = screen.getByAltText('Instagram FalaDev') 41 | expect(instagramLink).toBeTruthy() 42 | }) 43 | 44 | it('should render the footer with the YouTube link', () => { 45 | render( 46 | 47 |
Child Component
48 |
, 49 | ) 50 | 51 | const youtubeLink = screen.getByAltText('YouTube FalaDev') 52 | expect(youtubeLink).toBeTruthy() 53 | }) 54 | 55 | it('should render the footer with the WhatsApp link', () => { 56 | render( 57 | 58 |
Child Component
59 |
, 60 | ) 61 | 62 | const whatsappLink = screen.getByAltText('WhatsApp FalaDev') 63 | expect(whatsappLink).toBeTruthy() 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import Image from 'next/image' 4 | 5 | import './globals.css' 6 | import { cn } from '@/lib/utils' 7 | import { ReactQueryProvider } from '@/Provider/ReactQueryProvider' 8 | 9 | const inter = Inter({ subsets: ['latin'] }) 10 | 11 | export const metadata: Metadata = { 12 | title: 'FalaDev', 13 | description: 'Mentoria para desenvolvedores', 14 | icons: [{ url: '/static/imgs/faladev.ico' }], 15 | } 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode 21 | }>) { 22 | return ( 23 | 24 | 25 | 26 |
27 | FalaDev Logo 28 |
29 | {children} 30 | 59 |
60 | 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/ButtonWhiteBlack/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | interface ButtonWhiteBlackProps extends React.ButtonHTMLAttributes { 4 | children: ReactNode 5 | classList?: string 6 | } 7 | 8 | export const ButtonWhiteBlack = ({ children, classList, ...rest }: ButtonWhiteBlackProps) => { 9 | return ( 10 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/form/text-input.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | 3 | type TextInputProps = { 4 | label: string 5 | } & React.ComponentProps<'input'> 6 | 7 | // eslint-disable-next-line react/display-name 8 | export const TextInput = forwardRef(({ label, id, ...TextInputProps }, ref) => { 9 | return ( 10 |
11 | 14 | 20 |
21 | ) 22 | }) 23 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert-box/alert-boc.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { Status } from '@/app/(mentoring)/mentoring.type' 5 | 6 | import { AlertBox } from './' 7 | 8 | describe('AlertBox Component', () => { 9 | it('should render AlertBox with correct title and description for error status', () => { 10 | render() 11 | 12 | const alertTitle = screen.getByTestId('alert-title') 13 | const alertDescription = screen.getByTestId('alert-description') 14 | 15 | expect(alertTitle).toHaveTextContent('Error Title') 16 | expect(alertDescription).toHaveTextContent('Error Description') 17 | }) 18 | 19 | it('should render AlertBox with correct title and description for success status', () => { 20 | render() 21 | 22 | const alertTitle = screen.getByTestId('alert-title') 23 | const alertDescription = screen.getByTestId('alert-description') 24 | 25 | expect(alertTitle).toHaveTextContent('Success Title') 26 | expect(alertDescription).toHaveTextContent('Success Description') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert-box/alert-box.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { AlertBox } from './' 4 | 5 | const meta: Meta = { 6 | title: 'Components/UI/AlertBox', 7 | component: AlertBox, 8 | argTypes: { 9 | status: { 10 | control: { type: 'select', options: ['error', 'success'] }, 11 | description: 'Status of the alert, determines the variant', 12 | }, 13 | title: { 14 | control: 'text', 15 | description: 'Title text for the alert', 16 | }, 17 | description: { 18 | control: 'text', 19 | description: 'Description text for the alert', 20 | }, 21 | }, 22 | parameters: { 23 | layout: 'centered', 24 | }, 25 | } 26 | 27 | export default meta 28 | 29 | type AlertBoxStory = StoryObj 30 | 31 | export const ErrorAlert: AlertBoxStory = { 32 | args: { 33 | status: 'error', 34 | title: 'Error Title', 35 | description: 'This is an error message.', 36 | }, 37 | } 38 | 39 | export const SuccessAlert: AlertBoxStory = { 40 | args: { 41 | status: 'success', 42 | title: 'Success Title', 43 | description: 'This is a success message.', 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert-box/index.tsx: -------------------------------------------------------------------------------- 1 | import { RegistrationResult, Status } from '@/app/(mentoring)/mentoring.type' 2 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' 3 | 4 | type AlertBoxProps = RegistrationResult & React.HTMLAttributes 5 | 6 | type AlertVariant = 'destructive' | 'creation' 7 | 8 | const alertOptions: Record = { 9 | error: 'destructive', 10 | success: 'creation', 11 | } as const 12 | 13 | const alertVariant = (status: Status): AlertVariant => { 14 | return alertOptions[status] 15 | } 16 | 17 | export const AlertBox: React.FC = ({ status, title, description, ...props }) => { 18 | return ( 19 | 20 | {title} 21 | {description} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert/alert.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { Alert, AlertTitle, AlertDescription } from './' 5 | 6 | describe('Alert Component', () => { 7 | it('deve renderizar o Alert com a variante "destructive"', () => { 8 | render( 9 | 10 | 11 | Título do Alerta 12 | Descrição do Alerta 13 | , 14 | ) 15 | 16 | const alert = screen.getByRole('alert') 17 | expect(alert).toBeInTheDocument() 18 | 19 | const icon = screen.getByTestId('icon') 20 | expect(icon).toBeInTheDocument() 21 | 22 | const title = screen.getByText('Título do Alerta') 23 | expect(title).toBeInTheDocument() 24 | 25 | const description = screen.getByText('Descrição do Alerta') 26 | expect(description).toBeInTheDocument() 27 | }) 28 | 29 | it('deve renderizar o Alert com a variante "creation"', () => { 30 | render( 31 | 32 | 33 | Título do Alerta 34 | Descrição do Alerta 35 | , 36 | ) 37 | 38 | const alert = screen.getByRole('alert') 39 | expect(alert).toBeInTheDocument() 40 | 41 | const icon = screen.getByTestId('icon') 42 | expect(icon).toBeInTheDocument() 43 | 44 | const title = screen.getByText('Título do Alerta') 45 | expect(title).toBeInTheDocument() 46 | 47 | const description = screen.getByText('Descrição do Alerta') 48 | expect(description).toBeInTheDocument() 49 | }) 50 | 51 | it('deve renderizar AlertTitle e AlertDescription corretamente', () => { 52 | render( 53 | 54 | 55 | Título Personalizado 56 | Descrição Personalizada 57 | , 58 | ) 59 | 60 | const title = screen.getByText('Título Personalizado') 61 | expect(title).toBeInTheDocument() 62 | 63 | const description = screen.getByText('Descrição Personalizada') 64 | expect(description).toBeInTheDocument() 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert/alert.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Alert, AlertTitle, AlertDescription } from './index' 4 | 5 | type Story = StoryObj 6 | 7 | const meta: Meta = { 8 | component: Alert, 9 | title: 'Components/UI/Alert', 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | } 14 | export default meta 15 | 16 | export const Default: Story = { 17 | render: () => ( 18 | 19 |
20 | Default Alert 21 | This is a default alert description. 22 |
23 |
24 | ), 25 | } 26 | 27 | export const Destructive: Story = { 28 | render: () => ( 29 | 30 |
31 | Destructive Alert 32 | 33 | This is a destructive alert, indicating a serious issue. 34 | 35 |
36 |
37 | ), 38 | } 39 | 40 | export const Creation: Story = { 41 | render: () => ( 42 | 43 |
44 | Creation Alert 45 | This alert indicates a successful creation action. 46 |
47 |
48 | ), 49 | } 50 | 51 | export const WithoutIcon: Story = { 52 | render: () => ( 53 | 54 |
55 | Alert Without Icon 56 | This alert does not include an icon. 57 |
58 |
59 | ), 60 | } 61 | 62 | export const OnlyTitle: Story = { 63 | render: () => ( 64 | 65 | Alert With Only Title 66 | 67 | ), 68 | } 69 | 70 | export const OnlyDescription: Story = { 71 | render: () => ( 72 | 73 | This alert has only a description without a title. 74 | 75 | ), 76 | } 77 | 78 | export const CustomStyles: Story = { 79 | render: () => ( 80 | 81 |
82 | Custom Styled Alert 83 | This alert has custom background and text colors. 84 |
85 |
86 | ), 87 | } 88 | -------------------------------------------------------------------------------- /frontend/src/components/ui/alert/index.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 8 | { 9 | variants: { 10 | variant: { 11 | destructive: 12 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 13 | creation: 'border-green-600/70 text-green-700 [&>svg]:text-green-700', 14 | }, 15 | }, 16 | }, 17 | ) 18 | 19 | const Alert = React.forwardRef< 20 | HTMLDivElement, 21 | React.HTMLAttributes & VariantProps 22 | >(({ className, variant, ...props }, ref) => ( 23 |
24 | )) 25 | Alert.displayName = 'Alert' 26 | 27 | const AlertTitle = React.forwardRef>( 28 | ({ className, ...props }, ref) => ( 29 |
34 | ), 35 | ) 36 | AlertTitle.displayName = 'AlertTitle' 37 | 38 | const AlertDescription = React.forwardRef< 39 | HTMLParagraphElement, 40 | React.HTMLAttributes 41 | >(({ className, ...props }, ref) => ( 42 |
43 | )) 44 | AlertDescription.displayName = 'AlertDescription' 45 | 46 | export { Alert, AlertTitle, AlertDescription } 47 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button/button.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from '@testing-library/react' 2 | 3 | import { Button } from '.' 4 | 5 | describe('Button Component', () => { 6 | it('should render a button with default styles', () => { 7 | const screen = render() 8 | const button = screen.getByTestId('button') 9 | expect(button.className).toBe( 10 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2', 11 | ) 12 | }) 13 | 14 | it('should apply the correct classes based on variant', () => { 15 | render() 16 | const button = screen.getByText('Delete') 17 | expect(button.className).toBe( 18 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2', 19 | ) 20 | }) 21 | 22 | it('should disable the button when `disabled` prop is true', () => { 23 | render() 24 | const button = screen.getByText('Disabled Button') as HTMLButtonElement 25 | expect(button.disabled).toBe(true) 26 | }) 27 | 28 | it('should render as a Slot when `asChild` prop is true', () => { 29 | const { container } = render( 30 | , 33 | ) 34 | const element = container.querySelector('[data-testid="custom-element"]') 35 | expect(element).toBeTruthy() 36 | expect(element?.tagName).toBe('SPAN') 37 | }) 38 | 39 | it('should trigger onClick handler when clicked', () => { 40 | const handleClick = vi.fn() 41 | render() 42 | const button = screen.getByText('Click Me') 43 | fireEvent.click(button) 44 | expect(handleClick).toHaveBeenCalledTimes(1) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Button } from './index' 4 | 5 | export type Story = StoryObj 6 | 7 | const meta: Meta = { 8 | component: Button, 9 | title: 'Components/UI/Button', 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | } 14 | export default meta 15 | 16 | export const Default: StoryObj = { 17 | render: () => , 18 | } 19 | 20 | export const Destructive: StoryObj = { 21 | render: () => , 22 | } 23 | 24 | export const Ghost: StoryObj = { 25 | render: () => , 26 | } 27 | 28 | export const Link: StoryObj = { 29 | render: () => , 30 | } 31 | 32 | export const Outline: StoryObj = { 33 | render: () => , 34 | } 35 | 36 | export const Secondary: StoryObj = { 37 | render: () => , 38 | } 39 | 40 | export const SM: StoryObj = { 41 | render: () => , 42 | } 43 | 44 | export const LG: StoryObj = { 45 | render: () => , 46 | } 47 | 48 | export const Icon: StoryObj = { 49 | render: () => , 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button/index.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | import * as React from 'react' 4 | 5 | const buttonVariants = cva( 6 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 11 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 12 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 13 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | ghost: 'hover:bg-accent hover:text-accent-foreground', 15 | link: 'text-primary underline-offset-4 hover:underline', 16 | }, 17 | size: { 18 | default: 'h-10 px-4 py-2', 19 | sm: 'h-9 rounded-md px-3', 20 | lg: 'h-11 rounded-md px-8', 21 | icon: 'h-10 w-10', 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: 'default', 26 | size: 'default', 27 | }, 28 | }, 29 | ) 30 | 31 | export interface ButtonProps 32 | extends React.ButtonHTMLAttributes, 33 | VariantProps { 34 | asChild?: boolean 35 | } 36 | 37 | const Button = React.forwardRef( 38 | ({ className, variant, size, asChild = false, ...props }, ref) => { 39 | const Comp = asChild ? Slot : 'button' 40 | return 41 | }, 42 | ) 43 | Button.displayName = 'Button' 44 | 45 | export { Button, buttonVariants } 46 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card/card.spec.tsx: -------------------------------------------------------------------------------- 1 | // Card.test.tsx 2 | import React from 'react' 3 | import { render, screen } from '@testing-library/react' 4 | import { describe, it, expect, vi } from 'vitest' 5 | import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from '.' 6 | 7 | describe('Card Components', () => { 8 | it('should render Card component correctly', () => { 9 | render(Card Content) 10 | const cardElement = screen.getByText('Card Content') 11 | expect(cardElement.className).toBe('rounded-lg border bg-card text-card-foreground shadow-sm') 12 | }) 13 | 14 | it('should render CardHeader component correctly', () => { 15 | render(Card Header) 16 | const cardHeaderElement = screen.getByText('Card Header') 17 | expect(cardHeaderElement.className).toBe('flex flex-col space-y-1.5 p-6') 18 | }) 19 | 20 | it('should render CardTitle component correctly', () => { 21 | render(Card Title) 22 | const cardTitleElement = screen.getByText('Card Title') 23 | expect(cardTitleElement.className).toBe('text-2xl font-semibold leading-none tracking-tight') 24 | }) 25 | 26 | it('should render CardDescription component correctly', () => { 27 | render(Card Description) 28 | const cardDescriptionElement = screen.getByText('Card Description') 29 | expect(cardDescriptionElement.className).toBe('text-sm text-muted-foreground') 30 | }) 31 | 32 | it('should render CardContent component correctly', () => { 33 | render(Card Content) 34 | const cardContentElement = screen.getByText('Card Content') 35 | expect(cardContentElement.className).toBe('p-6 pt-0') 36 | }) 37 | 38 | it('should render CardFooter component correctly', () => { 39 | render(Card Footer) 40 | const cardFooterElement = screen.getByText('Card Footer') 41 | expect(cardFooterElement.className).toBe('flex items-center p-6 pt-0') 42 | }) 43 | 44 | it('should forward ref correctly', () => { 45 | const ref = vi.fn() 46 | render(Card Content) 47 | expect(ref).toHaveBeenCalled() 48 | expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLDivElement) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card/card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './index' 4 | 5 | type Story = StoryObj 6 | 7 | const meta: Meta = { 8 | component: Card, 9 | title: 'Components/UI/Card', 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | } 14 | export default meta 15 | 16 | export const Default: Story = { 17 | render: () => ( 18 | 19 | 20 | Default Card 21 | This is the default card description. 22 | 23 | 24 |

This is the content inside the default card.

25 |
26 | 27 | 28 | 29 |
30 | ), 31 | } 32 | 33 | export const WithLongContent: Story = { 34 | render: () => ( 35 | 36 | 37 | Card with Long Content 38 | 39 | Here is some lengthy description text to show how the card handles more content. 40 | 41 | 42 | 43 |

44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus imperdiet, nulla et 45 | dictum interdum, nisi lorem egestas odio, vitae scelerisque enim ligula venenatis dolor. 46 | Maecenas nisl est, ultrices nec congue eget, auctor vitae massa. 47 |

48 |
49 | 50 | 51 | 52 |
53 | ), 54 | } 55 | 56 | export const WithImage: Story = { 57 | render: () => ( 58 | 59 | 60 | Placeholder Image 61 | Card with Image 62 | This card has an image in the header. 63 | 64 | 65 |

This is the content of a card with an image.

66 |
67 | 68 | 69 | 70 |
71 | ), 72 | } 73 | 74 | export const WithoutFooter: Story = { 75 | render: () => ( 76 | 77 | 78 | Card Without Footer 79 | This card does not have a footer. 80 | 81 | 82 |

This is the content of a card without a footer.

83 |
84 |
85 | ), 86 | } 87 | 88 | export const CustomStyles: Story = { 89 | render: () => ( 90 | 91 | 92 | Custom Styled Card 93 | This card has custom styles applied. 94 | 95 | 96 |

This is the content of a card with custom styles.

97 |
98 | 99 | 100 | 101 |
102 | ), 103 | } 104 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /frontend/src/components/ui/error-message/error-message.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import { ErrorMessage } from './' 4 | 5 | const mockErrors = { 6 | email: { 7 | message: 'O campo de e-mail é obrigatório', 8 | }, 9 | password: { 10 | message: 'A senha deve ter pelo menos 8 caracteres', 11 | }, 12 | } 13 | 14 | describe('ErrorMessage', () => { 15 | it('deve exibir a mensagem de erro para o campo de email', () => { 16 | render() 17 | 18 | const errorMessage = screen.getByTestId('error-message') 19 | 20 | expect(errorMessage.textContent).toBe('O campo de e-mail é obrigatório') 21 | }) 22 | 23 | it('deve exibir a mensagem de erro para o campo de senha', () => { 24 | render() 25 | 26 | const errorMessage = screen.getByTestId('error-message') 27 | 28 | expect(errorMessage.textContent).toBe('A senha deve ter pelo menos 8 caracteres') 29 | }) 30 | 31 | it('não deve exibir mensagem de erro se o campo não tiver erro', () => { 32 | render() 33 | 34 | const errorMessage = screen.queryByTestId('error-message') ?? null 35 | expect(errorMessage).toBeNull() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /frontend/src/components/ui/error-message/error-message.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { ErrorMessage } from './' 4 | 5 | const mockErrors = { 6 | email: { 7 | message: 'O campo de e-mail é obrigatório', 8 | }, 9 | password: { 10 | message: 'A senha deve ter pelo menos 8 caracteres', 11 | }, 12 | } 13 | 14 | const meta: Meta = { 15 | component: ErrorMessage, 16 | title: 'Components/UI/ErrorMessage', 17 | parameters: { 18 | layout: 'centered', 19 | }, 20 | argTypes: { 21 | name: { 22 | control: 'text', 23 | description: 'Nome do campo do formulário para o qual exibir a mensagem de erro', 24 | }, 25 | errors: { 26 | control: 'object', 27 | description: 'Objeto contendo os erros do formulário', 28 | }, 29 | }, 30 | } 31 | export default meta 32 | 33 | type Story = StoryObj 34 | 35 | export const EmailError: Story = { 36 | args: { 37 | name: 'email', 38 | errors: mockErrors, 39 | }, 40 | } 41 | 42 | export const PasswordError: Story = { 43 | args: { 44 | name: 'password', 45 | errors: mockErrors, 46 | }, 47 | } 48 | 49 | export const NoError: Story = { 50 | args: { 51 | name: 'username', 52 | errors: mockErrors, 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/components/ui/error-message/index.tsx: -------------------------------------------------------------------------------- 1 | export const ErrorMessage = ({ 2 | errors, 3 | name, 4 | }: { 5 | errors: Record 6 | name: string 7 | }) => { 8 | const existError = errors && Object.keys(errors).includes(name) 9 | 10 | return ( 11 | <> 12 | {existError && ( 13 |
17 |

18 | {errors[name].message} 19 |

20 |
21 | )} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Input } from '.' 4 | 5 | export type Story = StoryObj 6 | 7 | const meta: Meta = { 8 | component: Input, 9 | title: 'Components/UI/Input', 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | } 14 | export default meta 15 | 16 | export const Default: Story = { 17 | render: () => { 18 | return 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export interface InputProps extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 20 | ) 21 | }, 22 | ) 23 | Input.displayName = 'Input' 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input/input.spec.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/react' 2 | import { Input } from '.' 3 | 4 | import React from 'react' 5 | 6 | describe('', () => { 7 | test('applies custom class', () => { 8 | const screen = render() 9 | const inputElement: HTMLInputElement = screen.getByTestId('textbox') as HTMLInputElement 10 | expect(inputElement.id).toBe('custom-class') 11 | }) 12 | 13 | test('input is disabled', () => { 14 | const screen = render() 15 | const inputElement: HTMLInputElement = screen.getByTestId('textbox') as HTMLInputElement 16 | expect(inputElement.disabled).toBe(true) 17 | }) 18 | 19 | test('displays correct value', () => { 20 | const screen = render() 21 | const inputElement: HTMLInputElement = screen.getByTestId('textbox') as HTMLInputElement 22 | expect(inputElement.value).toBe('Value') 23 | }) 24 | 25 | test('updates value correctly', () => { 26 | const { getByTestId } = render() 27 | const inputElement: HTMLInputElement = getByTestId('textbox') as HTMLInputElement 28 | 29 | fireEvent.change(inputElement, { target: { value: 'New Value' } }) 30 | expect(inputElement.value).toBe('New Value') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label" 4 | import { cva, type VariantProps } from "class-variance-authority" 5 | import * as React from 'react' 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label/label.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import React from 'react' 3 | import { describe, it, expect, vi } from 'vitest' 4 | 5 | import { Label } from '.' 6 | 7 | describe('Label Component', () => { 8 | it('should render correctly', () => { 9 | const labelText = 'Test Label' 10 | render() 11 | 12 | const labelElement = screen.getByTestId('label') 13 | expect(labelElement.textContent).toBe(labelText) 14 | }) 15 | 16 | it('should forward ref correctly', () => { 17 | const ref = vi.fn() 18 | 19 | render() 20 | 21 | expect(ref).toHaveBeenCalled() 22 | expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLLabelElement) 23 | }) 24 | 25 | it('should apply className and labelVariants correctly', () => { 26 | render() 27 | 28 | const labelElement = screen.getByText('Test Label') 29 | expect(labelElement.id).toBe('custom-class') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label/label.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Label } from './' 4 | 5 | const meta: Meta = { 6 | component: Label, 7 | title: 'Components/UI/Label', 8 | argTypes: { 9 | className: { 10 | control: 'text', 11 | description: 'Adicione classes adicionais para estilização', 12 | }, 13 | }, 14 | parameters: { 15 | layout: 'centered', 16 | }, 17 | } 18 | export default meta 19 | 20 | type Story = StoryObj 21 | 22 | export const Default: Story = { 23 | render: () => , 24 | } 25 | 26 | export const Disabled: Story = { 27 | render: () => , 28 | args: { 29 | className: 'peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 30 | }, 31 | } 32 | 33 | export const CustomStyle: Story = { 34 | render: () => , 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/upload/MockFormProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm, FormProvider, UseFormReturn } from 'react-hook-form'; 3 | 4 | interface MockFormProviderProps { 5 | children: React.ReactNode; 6 | defaultValues?: Record; 7 | methods?: Partial; 8 | } 9 | 10 | export const MockFormProvider: React.FC = ({ children, defaultValues = {}, methods = {} }) => { 11 | const formMethods = useForm({ defaultValues }); 12 | const combinedMethods = { ...formMethods, ...methods }; 13 | 14 | return {children}; 15 | }; -------------------------------------------------------------------------------- /frontend/src/components/upload/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Image from 'next/image' 3 | import { useRef, useState } from 'react' 4 | import { useDropzone } from 'react-dropzone' 5 | import { Controller, useFormContext } from 'react-hook-form' 6 | 7 | import { ButtonWhiteBlack } from '../Buttons/ButtonWhiteBlack' 8 | 9 | const ZERO = 0 10 | 11 | interface UploadProps { 12 | onChange: (...event: any[]) => void, 13 | name:string 14 | } 15 | 16 | export const Upload =({name='upload', ...rest}: {name?: string}) =>{ 17 | const { control, } = useFormContext() 18 | 19 | return( 20 | ( 22 | 23 | onChange(e.target.files[ZERO])} name={name} {...rest}/> 24 | )} 25 | name={name} 26 | control={control} 27 | defaultValue="" 28 | /> 29 | ) 30 | } 31 | 32 | const UploadInput = ({ onChange,name, ...rest 33 | }: UploadProps) => { 34 | const [imgPreviewUrl, setImgPreviewUrl] = useState(null) 35 | const inputImageRef = useRef(null); 36 | const { setValue, } = useFormContext() 37 | 38 | const handleRemoveImage = () => { 39 | setImgPreviewUrl(null) 40 | if (inputImageRef.current) { 41 | inputImageRef.current.value = ''; 42 | setValue(name,'') 43 | } 44 | } 45 | const onDrop = (acceptedFiles: File[]) => { 46 | const file = acceptedFiles[ZERO]; 47 | if (file) { 48 | setImgPreviewUrl( URL.createObjectURL(file)); 49 | setValue(name,file) 50 | } 51 | }; 52 | const { getRootProps, getInputProps } = useDropzone({ onDrop, ...rest 53 | }); 54 | return ( 55 |
56 |
57 | {imgPreviewUrl && ×} 58 |
59 |
{inputImageRef.current && inputImageRef.current.click()}} 63 | > 64 | 65 | 74 | {imgPreviewUrl ? ( 75 | photo preview 76 | ) : ( 77 | person unknow 84 | )} 85 |
86 |
87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /frontend/src/components/upload/upload.spec.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from "@testing-library/react" 2 | import userEvent from '@testing-library/user-event' 3 | import { expect } from "vitest"; 4 | 5 | import { FormStateChecker, renderWithFormProvider } from "@/test-utils"; 6 | 7 | import { Upload } from "." 8 | 9 | 10 | const ONE = 1; 11 | describe('Upload Component',()=>{ 12 | beforeEach(()=>{ 13 | global.URL.createObjectURL = ()=>'http://localhost:3000/e17fa82b-b0c5-4b24-82fc-132a8659d6adimg/preview/test' 14 | renderWithFormProvider( 15 | <> 16 | 17 | 18 | 19 | ) 20 | }) 21 | it("should render the component without photo",()=>{ 22 | const formState = screen.getByTestId("form-state"); 23 | expect(screen.getByAltText("person unknow")).toBeInTheDocument(); 24 | expect(formState.textContent).toBe(''); 25 | }) 26 | it("should handle file upload", async()=>{ 27 | const inputUpload = screen.getByLabelText(/upload file/i); 28 | const file = new File(['hello'], 'hello.png', {type: 'image/png'}) 29 | expect(inputUpload).toBeInTheDocument(); 30 | userEvent.upload(inputUpload,file); 31 | await waitFor(() => { 32 | expect(inputUpload.files?.[0]).toStrictEqual(file); 33 | expect(inputUpload.files?.item(0)).toStrictEqual(file); 34 | expect(inputUpload.files).toHaveLength(ONE) 35 | }); 36 | }) 37 | it("should handle image removal", async()=>{ 38 | const inputUpload = screen.getByLabelText(/upload file/i); 39 | const file = new File(['hello'], 'hello.png', {type: 'image/png'}) 40 | userEvent.upload(inputUpload,file); 41 | await waitFor(()=>{ 42 | expect(inputUpload.files?.[0]).toStrictEqual(file); 43 | expect(inputUpload.files?.item(0)).toStrictEqual(file); 44 | expect(inputUpload.files).toHaveLength(ONE) 45 | }) 46 | const photoPreview = screen.getByAltText(/photo preview/i); 47 | const buttonRemoveImage = screen.getByTestId('custom-element'); 48 | expect(photoPreview).toBeInTheDocument() 49 | expect(buttonRemoveImage).toBeInTheDocument() 50 | userEvent.click(buttonRemoveImage); 51 | await waitFor(()=>{ 52 | expect(inputUpload.files?.[0]).toStrictEqual(undefined); 53 | expect(inputUpload.files?.item(0)).toStrictEqual(null); 54 | expect(inputUpload.files).toHaveLength(0) 55 | }) 56 | expect(screen.getByAltText("person unknow")).toBeInTheDocument(); 57 | }) 58 | }) -------------------------------------------------------------------------------- /frontend/src/components/upload/upload.stories.tsx: -------------------------------------------------------------------------------- 1 | 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import { Upload } from "./index"; 5 | import { MockFormProvider as FormProvider } from './MockFormProvider'; 6 | 7 | type Story = StoryObj; 8 | 9 | const meta: Meta = { 10 | component: Upload, 11 | title: 'Components/Upload', 12 | argTypes:{ 13 | name:{ 14 | control:{type:'text'}, 15 | description: 'Field identifier in React hook form' 16 | } 17 | }, 18 | parameters: { 19 | layout: 'centered' 20 | } 21 | }; 22 | export default meta 23 | 24 | export const Default:Story = { 25 | render: () => ( 26 | 27 | 28 | 29 | ) 30 | } -------------------------------------------------------------------------------- /frontend/src/infra/http/HttpClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from 'vitest' 2 | import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios' 3 | import { HttpMethod, HttpRequest } from './HttpClient.types' 4 | import { HttpAxiosAdapter } from './HttpClient' 5 | 6 | // Mock do axios 7 | vi.mock('axios', () => ({ 8 | default: { 9 | request: vi.fn(), 10 | }, 11 | })) 12 | 13 | describe('HttpAxiosAdapter', () => { 14 | let mockAxios: { request: vi.Mock } 15 | let httpClient: HttpAxiosAdapter 16 | 17 | beforeEach(() => { 18 | mockAxios = axios as unknown as { request: vi.Mock } 19 | httpClient = new HttpAxiosAdapter(mockAxios as unknown as AxiosInstance) 20 | }) 21 | 22 | it('should return data when request is successful', async () => { 23 | const responseData = { success: true } 24 | const request: HttpRequest = { 25 | endpoint: '/test', 26 | method: HttpMethod.GET, 27 | } 28 | 29 | ;(mockAxios.request as vi.Mock).mockResolvedValueOnce({ 30 | data: responseData, 31 | } as AxiosResponse) 32 | 33 | const result = await httpClient.request(request) 34 | 35 | expect(result).toEqual(responseData) 36 | expect(mockAxios.request).toHaveBeenCalledWith({ 37 | method: HttpMethod.GET, 38 | headers: undefined, 39 | data: undefined, 40 | url: 'http://localhost:8080/api/test', 41 | }) 42 | }) 43 | 44 | it('should handle POST requests with body and headers', async () => { 45 | const responseData = { success: true } 46 | const request: HttpRequest = { 47 | endpoint: '/submit', 48 | method: HttpMethod.POST, 49 | body: { key: 'value' }, 50 | headers: { 'Content-Type': 'application/json' }, 51 | } 52 | 53 | ;(mockAxios.request as vi.Mock).mockResolvedValueOnce({ 54 | data: responseData, 55 | } as AxiosResponse) 56 | 57 | const result = await httpClient.request(request) 58 | 59 | expect(result).toEqual(responseData) 60 | expect(mockAxios.request).toHaveBeenCalledWith({ 61 | method: HttpMethod.POST, 62 | headers: { 'Content-Type': 'application/json' }, 63 | data: { key: 'value' }, 64 | url: 'http://localhost:8080/api/submit', 65 | }) 66 | }) 67 | 68 | it('should throw an error when the request fails', async () => { 69 | const request: HttpRequest = { 70 | endpoint: '/error', 71 | method: HttpMethod.GET, 72 | } 73 | 74 | const errorMessage = 'Request failed' 75 | const errorResponse = { 76 | response: { 77 | status: 500, 78 | data: errorMessage, 79 | }, 80 | } 81 | 82 | ;(mockAxios.request as vi.Mock).mockRejectedValueOnce(errorResponse as AxiosError) 83 | 84 | await expect(httpClient.request(request)).rejects.toThrow( 85 | `Request failed with status 500: ${errorMessage}`, 86 | ) 87 | 88 | expect(mockAxios.request).toHaveBeenCalledWith({ 89 | method: HttpMethod.GET, 90 | headers: undefined, 91 | data: undefined, 92 | url: 'http://localhost:8080/api/error', 93 | }) 94 | }) 95 | 96 | it('should handle network errors', async () => { 97 | const request: HttpRequest = { 98 | endpoint: '/network-error', 99 | method: HttpMethod.GET, 100 | } 101 | 102 | const networkError = new Error('Network Error') 103 | ;(mockAxios.request as vi.Mock).mockRejectedValueOnce(networkError) 104 | 105 | await expect(httpClient.request(request)).rejects.toThrow( 106 | `Request failed with status undefined: Network Error`, 107 | ) 108 | 109 | expect(mockAxios.request).toHaveBeenCalledWith({ 110 | method: HttpMethod.GET, 111 | headers: undefined, 112 | data: undefined, 113 | url: 'http://localhost:8080/api/network-error', 114 | }) 115 | }) 116 | 117 | it('should handle requests with no headers and body', async () => { 118 | const responseData = { success: true } 119 | const request: HttpRequest = { 120 | endpoint: '/no-header-no-body', 121 | method: HttpMethod.DELETE, 122 | } 123 | 124 | ;(mockAxios.request as vi.Mock).mockResolvedValueOnce({ 125 | data: responseData, 126 | } as AxiosResponse) 127 | 128 | const result = await httpClient.request(request) 129 | 130 | expect(result).toEqual(responseData) 131 | expect(mockAxios.request).toHaveBeenCalledWith({ 132 | method: HttpMethod.DELETE, 133 | headers: undefined, 134 | data: undefined, 135 | url: 'http://localhost:8080/api/no-header-no-body', 136 | }) 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /frontend/src/infra/http/HttpClient.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosInstance } from 'axios' 2 | 3 | import { HttpClient, HttpRequest } from './HttpClient.types' 4 | 5 | const URL = 'http://localhost:8080/api' 6 | 7 | export class HttpAxiosAdapter implements HttpClient { 8 | constructor(private api: AxiosInstance = axios) {} 9 | 10 | async request({ endpoint, method, body, headers }: HttpRequest): Promise { 11 | try { 12 | const { data } = await this.api.request({ 13 | method, 14 | headers, 15 | data: body, 16 | url: `${URL}${endpoint}`, 17 | }) 18 | 19 | return data 20 | } catch (er) { 21 | const error = er as AxiosError 22 | const status = error.response?.status 23 | const message = error.response?.data || error.message 24 | throw new Error(`Request failed with status ${status}: ${message}`) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/infra/http/HttpClient.types.ts: -------------------------------------------------------------------------------- 1 | export enum HttpMethod { 2 | GET = 'get', 3 | POST = 'post', 4 | PUT = 'put', 5 | DELETE = 'delete', 6 | } 7 | 8 | export type HttpRequest = { 9 | endpoint: string 10 | method: HttpMethod 11 | body?: any 12 | headers?: any 13 | } 14 | 15 | export interface HttpClient { 16 | request: (request: HttpRequest) => Promise 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/lib/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { cn } from './utils' // Ajuste o caminho conforme necessário 3 | 4 | describe('cn', () => { 5 | it('should combine class names and remove duplicates', () => { 6 | expect(cn('bg-blue-500', 'text-white', 'bg-blue-500')).toBe('text-white bg-blue-500') 7 | }) 8 | 9 | it('should handle empty input', () => { 10 | expect(cn()).toBe('') 11 | expect(cn('')).toBe('') 12 | }) 13 | 14 | it('should handle multiple inputs', () => { 15 | expect(cn('bg-blue-500', 'text-white', ['p-4', 'm-2'])).toBe('bg-blue-500 text-white p-4 m-2') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/services/MentoringAgenda/MentoringAgenda.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | 3 | import { SchemaMentoringType } from '@/app/(mentoring)/mentoring.type' 4 | import { 5 | httpClientMockFail, 6 | httpClientMockSuccess, 7 | mockResponse, 8 | } from '@/tests/mock/httpClientMock' 9 | 10 | import { MentoringAgendaService } from './MentoringAgenda.service' 11 | 12 | const mockUserData: SchemaMentoringType = { 13 | name: 'John Doe', 14 | email: 'john.doe@example.com', 15 | phone: '1234567890', 16 | } 17 | 18 | describe('MentoringAgendaService', () => { 19 | test('should send a POST request to sign up for mentoring and return response', async () => { 20 | const mentoringAgendaService = new MentoringAgendaService(httpClientMockSuccess) 21 | const result = await mentoringAgendaService.SignUpMentoring(mockUserData) 22 | 23 | expect(httpClientMockSuccess.request).toHaveBeenCalledWith({ 24 | endpoint: '/events', 25 | method: 'post', 26 | body: mockUserData, 27 | }) 28 | 29 | expect(result).toBe(mockResponse) 30 | }) 31 | 32 | test('should throw an error if the request fails', async () => { 33 | const mentoringAgendaService = new MentoringAgendaService(httpClientMockFail) 34 | 35 | await expect(mentoringAgendaService.SignUpMentoring(mockUserData)).rejects.toThrow( 36 | 'Network error', 37 | ) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /frontend/src/services/MentoringAgenda/MentoringAgenda.service.ts: -------------------------------------------------------------------------------- 1 | import { SchemaMentoringType } from '@/app/(mentoring)/mentoring.type' 2 | import { HttpClient, HttpMethod } from '@/infra/http/HttpClient.types' 3 | 4 | export interface IMentoringAgendaService { 5 | SignUpMentoring: (data: SchemaMentoringType) => Promise 6 | } 7 | 8 | export class MentoringAgendaService implements IMentoringAgendaService { 9 | constructor(private readonly httpClient: HttpClient) {} 10 | 11 | async SignUpMentoring(userData: SchemaMentoringType): Promise { 12 | const rsponse = await this.httpClient.request({ 13 | endpoint: '/events', 14 | method: HttpMethod.POST, 15 | body: userData, 16 | }) 17 | return rsponse 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/services/User/User.service.ts: -------------------------------------------------------------------------------- 1 | import { UserSchema } from '@/app/(users)/register/user.schema' 2 | import { HttpClient, HttpMethod } from '@/infra/http/HttpClient.types' 3 | import { z } from 'zod' 4 | 5 | export interface IUserService { 6 | RegisterUser: (data: UserRegisterData) => Promise 7 | } 8 | 9 | export type UserRegisterData = z.infer 10 | 11 | export class UserService implements IUserService { 12 | constructor(private readonly httpClient: HttpClient) { } 13 | 14 | async RegisterUser(userData: UserRegisterData): Promise { 15 | const response = await this.httpClient.request({ 16 | endpoint: '/events', 17 | method: HttpMethod.POST, 18 | body: userData, 19 | }) 20 | 21 | return response 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/shared/registrationStatusMessages.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationResult, Status } from '../app/(mentoring)/mentoring.type' 2 | 3 | export const registrationStatusMessages: Record = { 4 | success: { 5 | status: 'success', 6 | title: 'Bem vindo à plataforma!', 7 | description: 'Você vai receber um email de confirmação em breve.', 8 | }, 9 | error: { 10 | status: 'error', 11 | title: 'Oops...', 12 | description: 'Ocorreu um erro durante seu cadastro.', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/test-utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import React, { FC, ReactElement } from "react"; 3 | import { FormProvider, useForm, useFormContext } from "react-hook-form" 4 | 5 | export const renderWithFormProvider = (ui:ReactElement) =>{ 6 | const Wrapper:FC<{ children: React.ReactNode }> = ( 7 | { children } 8 | )=>{ 9 | const methods = useForm(); 10 | return ( 11 | {children} 12 | ) 13 | }; 14 | 15 | return render(ui,{wrapper:Wrapper}) 16 | } 17 | 18 | export const FormStateChecker = ({ name }: {name: string }) =>{ 19 | const { getValues } = useFormContext(); 20 | return
{getValues(name)}
; 21 | }; -------------------------------------------------------------------------------- /frontend/src/tests/changeInput.tsx: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest' 2 | import { fireEvent } from '@testing-library/react' 3 | 4 | type ChangeInputProps = { 5 | input: HTMLInputElement 6 | valueInput: string 7 | } 8 | 9 | export const changeInput = ({ input, valueInput }: ChangeInputProps) => { 10 | fireEvent.change(input, { target: { value: valueInput } }) 11 | expect(input.value).toBe(valueInput) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/tests/mock/httpClientMock.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@/infra/http/HttpClient.types' 2 | export const mockResponse = 'Success' 3 | 4 | export const httpClientMockSuccess: HttpClient = { 5 | request: vi.fn().mockResolvedValue(mockResponse), 6 | } 7 | 8 | export const httpClientMockFail: HttpClient = { 9 | request: vi.fn().mockRejectedValue(new Error('Network error')), 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/tests/mock/mentoringServiceMock.ts: -------------------------------------------------------------------------------- 1 | import { IMentoringAgendaService } from '@/services/MentoringAgenda/MentoringAgenda.service' 2 | 3 | export const successfulMentoringServiceMock: IMentoringAgendaService = { 4 | SignUpMentoring: () => Promise.resolve('success'), 5 | } 6 | 7 | export const failedMentoringServiceMock: IMentoringAgendaService = { 8 | SignUpMentoring: () => Promise.reject(new Error('Error')), 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/tests/mock/mockSchemaMentoringTypeData.ts: -------------------------------------------------------------------------------- 1 | import { SchemaMentoringType } from '../../app/(mentoring)/mentoring.type' 2 | 3 | export const mockSchemaMentoringTypeData: SchemaMentoringType = { 4 | name: 'John Doe', 5 | email: 'johndoe@example.com', 6 | phone: '11960606060', 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/tests/mock/userServiceMock.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IUserService, UserRegisterData } from '@/services/User/User.service' 3 | 4 | export class MockUserService implements IUserService { 5 | RegisterUser: () => Promise = vi.fn() 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/tests/renderView.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/order 2 | import { render } from '@testing-library/react' 3 | import { ReactQueryProvider } from '@/Provider/ReactQueryProvider' 4 | 5 | export const renderView = (Element: React.ReactElement) => { 6 | return render({Element}) 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/tests/renderWithQueryClient.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | import { renderHook } from '@testing-library/react' 3 | 4 | export const renderWithQueryClient = (hook: () => T) => { 5 | const queryClient = new QueryClient() 6 | return renderHook(() => hook(), { 7 | wrapper: ({ children }: { children: React.ReactNode }) => ( 8 | {children} 9 | ), 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/tests/withReactQueryProvider.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | import React from 'react' 3 | 4 | export function withReactQueryProvider(Component: React.ComponentType) { 5 | return function WrapperComponent(props: T) { 6 | const queryClient = new QueryClient() 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate")], 78 | } satisfies Config 79 | 80 | export default config -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["vitest/globals", "@types/jest", "@testing-library/jest-dom"], 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"], 23 | "@components/*": ["./src/components/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /frontend/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 4000, // Substitua pela porta desejada 9 | host: true, // Se você deseja que o servidor seja acessível externamente 10 | }, 11 | resolve: { 12 | alias: [{ find: '@', replacement: resolve(__dirname, './src') }], 13 | }, 14 | test: { 15 | globals: true, 16 | environment: 'jsdom', 17 | coverage: { 18 | reporter: ['text', 'json', 'html'], 19 | all: true, 20 | include: ['src/**'], 21 | exclude: [ 22 | 'node_modules', 23 | 'tests', 24 | 'src/tests', 25 | '**/*.type.ts', 26 | '**/*.stories.tsx', 27 | '**/*page.tsx', 28 | 'src/app/exempla.tsx', 29 | ], 30 | }, 31 | setupFiles: './vitest.setup.mts', 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /frontend/vitest.setup.mts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | -------------------------------------------------------------------------------- /servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Servers": { 3 | "1": { 4 | "Name": "Postgres Docker", 5 | "Group": "Servers", 6 | "Host": "postgres", 7 | "Port": 5432, 8 | "MaintenanceDB": "faladev", 9 | "Username": "userpostgres", 10 | "Password": "passwordpostgres", 11 | "SSLMode": "prefer", 12 | "ConnectionTimeout": 10 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------