Passo a passo - orivaldosantana/mvp_banco_talentos GitHub Wiki

Mostrando como programar cada parte do sistema.

Criar um componente

Este componente recebe dois parâmetros, um título e um componente filho.

export default function ComponentX({ title, children }) {
  return (
    <div>
      <h2> {title} </h2>
      <div>
        {children}
      </div>
    </div>
  )
}

Criar uma nova página

Escolher o local na ramificação de pastas dentro da pasta app. As pastas neste diretório servem de rotas para o sistema. Por exemplo, a pasta app/admin/relatorio implementa a rota admin/relatorio. O nome do arquivo JavaScript deve ser page.tsx.

Menu lateral

O código a seguir apresenta um exemplo de menu lateral com três botões laterais construídos com o componente Link do Next.js. O estilo e a animação de cada botão estão no arquivo CSS em /app/page.module.css.

import Link from "next/link"
import styles from "../app/page.module.css"

export default function Navbar() {
  return (
    <div className={styles.menu}>
      <nav>
        <Link href="/admin/colaborador">Colaborador</Link>
        <Link href="/admin/projeto">Projeto</Link>
        <Link href="/admin/relatorio">Relatório</Link>
      </nav>
    </div>
  )
}

Criar um layout

O código a seguir está implementado em app/admin/layout.tsx e define layout com um menu lateral, um cabeçalho e uma página. Neste caso, as páginas que forem construídas no ramo da pasta app/admin/ terão a estrutura deste layout.

import Header from "@/ui/header"
import Navbar from "@/ui/navbar"
import { Box } from "@mui/material"

export default function PagesLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <div>
      <div>
        <Header />
      </div>
      <Box sx={{ flexDirection: "row", display: "flex" }}>
        <Navbar />
        {children}
      </Box>
    </div>
  )
}

Primeiros passos no Prisma

Criar a estrutura inicial do Prisma:

npx prisma init

O comando acima cria o arquivo schema.prisma e outros. O código a seguir apresentar um exemplo de schema.prisma para configurar o banco de dados para sqlite e definir uma tabela no banco de dados (esquema) para usuários (User).

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db" 
}

model User { 
  id Int @default(autoincrement()) @id 
  name String 
  email String
}

Cria a estrutura de arquivo SQLs para a construção ou atualização do banco de dados.

npx prisma migrate dev

Para acessar os banco de dados usando o Prisma Studio:

npx prisma studio

Mais detalhes acessar o link Manual Banco de Dados.

Gravando no banco de dados

Esta seção apresenta um código para cadastrar uma informação no banco de dados utilizando o conceito de Server Action do Next.js 14. O código a seguir obtêm os dados de um formulário contendo os campos nome e e-mail para gravar no banco de dados:

'use server'
import { PrismaClient } from '@prisma/client'

export const addUser = async (formData: FormData) => {
  let name = formData.get('nome')
  let email = formData.get('email')

  const prisma = new PrismaClient()

  try {
    await prisma.user.create({
      data: {
        name: name.toString(),
        email: email.toString(),

      }
    })
  } catch (error) {
    console.error(error)
  } finally {
    await prisma.$disconnect()
  }
}

O código acima pode ser organizado em um arquivo action.ts na pasta app\lib\.

No formulário é necessário adicionar o atributo action na tag form como a seguir:

      <form action={addUser}>

A função addUser foi declarada no código acima. Porém, para recuperar alguma mensagem vinda do backend será necessário incrementar esse código com a utilização do Hook useFormState.

Exemplo do código completo do componente que utilizar o formulário:

import { addUser } from "@/app/lib/action"
import TextField from "@mui/material/TextField"
import Box from "@mui/material/Box"
import Button from "@mui/material/Button" // Add this line to import the Button component
import { Typography } from "@mui/material"
import { useFormState } from 'react-dom'


export default function FormColaborador({ title }) {
  const [state, formAddUserAction] = useFormState(addUser, undefined)
  return (
    <Box
      sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
    >
      <Typography variant="h4" sx={{ marginBottom: 2 }}>
        {title}
      </Typography>
      <form action={formAddUserAction}>
        <TextField
          name="nome"
          label="Nome"
          fullWidth
          required
          variant="filled"
          sx={{ marginBottom: 2 }}
        />
        <TextField
          name="email"
          label="Email"
          fullWidth
          required
          variant="filled"
          sx={{ marginBottom: 2 }}
        />
        <Button variant="contained" type="submit" fullWidth>
          Cadastrar Colaborador
        </Button>
      </form>
    </Box>
  )
}

A função de cadastro no banco de dados, addUser, deve ser adicionada como parâmetro na inicialização do Hook useFormState, assim como variável initialState que armazena uma mensagem inicial que pode ser enviada para addUser. O retorno do Hook useFormState é um vetor com a variável, mensagem de retorno depois da execução de addUser, e uma função ,action, usada para chamar uma ação do servidor por meio da formAddUserAction.

Para realizar o Hook useFormState é necessário importar:

import { useFormState } from 'react-dom'

No código do formulário no atributo action usar o formAddUserAction definido na inicialização do useFormState:

        <form action={formAddUserAction}>

Recuperando dados do banco de dados

Criar um arquivo separado para organizar as funções relativas à recuperação dos dados no banco de dados. Sugestão para o caminho e nome do arquivo, src/app/lib/data.ts:

"use server";
import { PrismaClient } from "@prisma/client";

export const getUserData = async () => {
  const prisma = new PrismaClient();
  let users;

  try {
    users = await prisma.user.findMany();
    console.log(users);
  } catch (error) {
    console.error(error);
  } finally {
    await prisma.$disconnect();
    return users; 
  }
};

Para mostrar os dados em uma página, podemos realizar uma chamada da função criada acima, getUserData, em uma página ou um componente do frontend, exemplo:

import { getUserData } from "@/app/lib/data";

export default async function RelatorioPage() {
  const users = await getUserData();

  return (
    <>
      {users !== undefined && (
        <div>
          {users.map((user) => (
            <div key={user.name}>{user.name}</div>
          ))}
        </div>
      )}
    </>
  );
}

Mostrar uma mensagem de sucesso ou erro ao gravar um dado

Vamos ajustar o código inicial para ficar compatível com o Hook useFormState. Na função de que realiza o cadastro vamos adicionar mais um parâmetro na definição da função, o prevState do tipo {message: string}:

'use server'
import { PrismaClient } from '@prisma/client'

export const addUser = async (
  prevState: {
    message: string
  },
  formData: FormData
) => {
  let name = formData.get('nome')
  let email = formData.get('email')
  let returnMessage 

  const prisma = new PrismaClient()

  try {
    await prisma.user.create({
      data: {
        name: name.toString(),
        email: email.toString(),

      }
    })
    returnMessage = { message: 'Cadastro realizado com sucesso!', type: 'success' }
  } catch (error) {
    returnMessage = { message: 'Erro ao realizar o cadastro!', type: 'error' }
    console.error(error)
  } finally {
    await prisma.$disconnect()
    return returnMessage
  }
}

No componente que realiza a chamada para o cadastro, vamos adicionar a inicialização do Hook useFormState:

  const [state, formAddUserAction] = useFormState(addUser, initialState)

A variável initialState deve ser definida no início do código do componente, logo depois das importações:

const initialState = {
  message: '',
  type: ''
}

Mostrar um exemplo do código completo para o componente relacionado com o cadastro:

'use client'
import { addUser } from '@/app/lib/action'
import TextField from '@mui/material/TextField'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button' // Add this line to import the Button component
import { IconButton, Typography, Alert, Collapse } from '@mui/material'
import { Close } from '@mui/icons-material'
import { useFormState } from 'react-dom'
import { useEffect, useState } from 'react' // Add this line to import the useEffect hook

const initialState = {
  message: ''
}

export default function FormColaborador({ title }) {
  const [state, formAddUserAction] = useFormState(addUser, initialState)
  const [openAlert, setOpenAlert] = useState(false)

  useEffect(() => {
    if (state?.message) setOpenAlert(true)
  }, [state])

  return (
    <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }} >
      <Typography variant="h4" sx={{ marginBottom: 2 }}>
        {title}
      </Typography>
      <form action={formAddUserAction}>
        <TextField name="nome" label="Nome" fullWidth required variant="filled" sx={{ marginBottom: 2 }} >
        <TextField name="email" label="Email" fullWidth required variant="filled" sx={{ marginBottom: 2 }} />
        <Button variant="contained" type="submit" fullWidth sx={{ marginTop: 2, marginBottom: 4 }}>
          Cadastrar Colaborador
        </Button>
        <Collapse in={openAlert}>
          <Alert
            severity={state?.type}
            action={
              <IconButton onClick={() => setOpenAlert(false)}>
                <Close />
              </IconButton>
            }
          >
            {state?.message}
          </Alert>
        </Collapse>
      </form>
    </Box>
  )
}

Redirecionamento de página

Depois de executar alguma ação e desejar redirecionar a aplicação para outra página, podemos usar o Next Router:

Inicialização:

import { useRouter } from 'next/navigation'
...
function NomeDoComponente() {
   ...
   const router = useRouter()
   ...

Utilização:

   ...
   router.push('/') 

Criptografando a senha

Importando a biblioteca:

import bcrypt from 'bcryptjs'
...

Utilização:

  ...
  const salt = await bcrypt.genSalt(10)
  const hashedPassword = await bcrypt.hash(password, salt)
  ...

Login com credenciais e Auth.js

  1. Use o hook useFormState crie um componente para o formulário de login com os campos usuário e senha.
  2. Adicione no arquivo lib/action.ts a função para tratar os dados de login. Use a função signIn com parâmetros 'credentials', { email, password }, exemplo de código:
export async function handleCredentialLogin(
  prevState: {
    message: string
  },
  formData: FormData
) {
  let email = formData.get('email')
  let password = formData.get('password')

  try {
    const user = await signIn('credentials', { email, password })
    return { message: 'Login realizado com sucesso!', type: 'success' }
  } catch (error) {
    if (error.type?.includes('CredentialsSignin')) {
      return { message: 'E-mail ou senha inválidos!', type: 'error' }
    }
    throw error
  }
}
  1. No arquivo lib/auth.ts, importe o provider para login com credenciais, credentials, e adicionar no vetor de configurações de providers do NextAuth. A lógica aqui é simples, se encontrar o usuário do banco de dados e se a senha estiver correta, retorna este usuário, se ocorrer algum erro, retorna nulo:
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
...
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: {},
        password: {},
      },
      authorize: async (credentials) => {
        try {
          const user = await loginCredential(credentials)
          return user
        } catch (error) {
          return null
        }
      },
    }),
  ],
})
  1. Implemente a lógica de login na função loginCredential. Sugestão de lógica, buscar o usuário no banco de dados, se não encontrar, lançe um erro, compare a senha informada com a senha do banco criptografada, lançe um erro se forem diferentes, se ocorrer algum erro, lançe. Ao final, retorne o usuário:
const loginCredential = async (credentials) => {
  const prisma = new PrismaClient()
  let user = null
  try {
    user = await prisma.user.findFirst({
      where: {
        email: credentials.email
      }
    })
    await prisma.$disconnect()
    if (!user) {
      throw new Error('Credenciais inválidas.')
    }
    const match = await bcrypt.compare(credentials.password, user.password)
    if (!match) {
      user = null
      throw new Error('Credenciais inválidas..')
    }
  } catch (error) {
    throw new Error('Falha ao realizar o login.')
  } finally {
    await prisma.$disconnect()
    return user
  }
}

Acesse a documentação oficial aqui.

Login com GitHub e Auth.js

  1. Configure o Github para autorizar a autentição e obter Client ID e Client Secret.
  2. Guarde estes valores no arquivo de variáveis de ambiente, .local.env:
AUTH_GITHUB_ID={CLIENT_ID}
AUTH_GITHUB_SECRET={CLIENT_SECRET}
  1. No arquivo lib/auth.ts, importe o provider para login com Github (OAuth), GitHub, e adicione no vetor de configurações de providers do NextAuth:
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
})
  1. Adicione ao seu arquivo api/auth/[...nextauth]/route.ts, os handlers que o NextAuth retorna, para que o Auth.js possa ser executado em qualquer solicitação recebida.
import { handlers } from "@/auth"
export const { GET, POST } = handlers
  1. Use o hook useFormState e crie um componente para o formulário de login com os campos usuário e senha.
  2. Adicione no arquivo lib/action.ts a função handleGitHubLogin para tratar os dados de login, exemplo de código:
export const handleGitHubLogin = async () => {
  'use server'
  await signIn('github')
}
  1. No arquivo lib/auth.ts configurar o ClientId, ClientSecret, e callback:
 callbacks: {
    async signIn({ user, account, profile }) {
      if (account.provider === 'github') {
         // faz alguma coisa se for login via github 
         ...
      }
      return true
    }
  }
  1. Cadastrando o usuário quando realizar login via GitHub:
 callbacks: {
    async signIn({ user, account, profile }) {
      if (account.provider === 'github') {
        const prisma = new PrismaClient()
        try {
          let userBD = await prisma.user.findFirst({
            where: {
              email: user.email.toString()
            }
          })
          if (userBD) {
            console.log('E-mail já cadastrado!')
          } else {
            await prisma.user.create({
              data: {
                name: user.name.toString(),
                email: user.email.toString(),
              }
            })
          }
        } catch (error) {
          console.log('Erro ao realizar o cadastro!')
        } finally {
          await prisma.$disconnect()
          return true
        }
      }
      return true
    }
  }

Acesse a documentação oficial aqui.

Referências

⚠️ **GitHub.com Fallback** ⚠️