gilbarbara / react-joyride

Create guided tours in your apps
https://react-joyride.com/
MIT License
6.62k stars 519 forks source link

Clicking outsite bugging on production server #1017

Closed thealdoamati closed 3 months ago

thealdoamati commented 3 months ago

🐛 Bug Report

I'm having a problem when I upload my project to production, when I click outside the joyride, in my dev branch It has the red circle image When I click outside the joyride in production, it get like this image It's like I pass to the next step, it loads without the content

It's not even showing the red dot like my dev branch image image

This are my steps, the only one that works is the first one with the placement center

import { Step, Placement } from 'react-joyride'
import React from 'react'

interface StepContentProps {
  text: string
}

export const StepContent: React.FC<StepContentProps> = ({ text }) => {
  return (
    <>
      {text.split('\n').map((line, index) => (
        <React.Fragment key={index}>
          {line}
          <br />
        </React.Fragment>
      ))}
    </>
  )
}

export const allIntegrationsSteps: Step[] = [
  {
    title: 'Bem-vindo à plataforma da Scalable!',
    content:
      'Para começar a criar os seus dashboards, você deve primeiro inserir alguns dados',
    target: '#joyride-welcome',
    placement: 'center' as Placement,
  },
  {
    title: 'Integre ou Adicione manualmente',
    content:
      'Crie seus dashboards a partir do seu meio de pagamento, ou cadastre os contratos de seus clientes manualmente.',
    target: '#joyride-docs',
    placement: 'auto' as Placement,
  },
]

export const manualIntegrationsSteps: Step[] = [
  {
    title: 'Crie dashboards manualmente',
    content: (
      <StepContent
        text={
          '1) Para gerar os seus dashboards, cadastre os contratos dos seus clientes.\n\nAqui você também irá adicionar os pagamentos passados e futuros de seus clientes.'
        }
      />
    ),
    target: '#joyride-contratos',
    placement: 'auto' as Placement,
  },
  {
    title: 'Cadastre seus clientes',
    content: (
      <StepContent
        text={
          '2) Aqui você pode criar novos clientes e salva-los para automatizar o cadastro de novas compras de plano no futuro.\n\nObs: Você também pode cria-los dentro de um novo contrato.'
        }
      />
    ),
    target: '#joyride-clientes',
    placement: 'auto' as Placement,
  },
  {
    title: 'Cadastre seus planos',
    content: (
      <StepContent
        text={
          '3) Aqui você pode criar seus  planos recorrentes, colocando MTR, período etc para automatizar  o cadastro de novas contratações.\n\nObs: Você também pode cria-los dentro de um novo contrato.'
        }
      />
    ),
    target: '#joyride-planos',
    placement: 'auto' as Placement,
  },
  {
    title: 'Administre seus pagamentos',
    content: (
      <StepContent
        text={
          '4) Aqui você irá gerenciar os pagamentos dos seus clientes criados da área de novos contratos.\n\nVocê pode mudar o status de pago e não pago (e isso reflitirá nos gráficos).'
        }
      />
    ),
    target: '#joyride-payment',
    placement: 'auto' as Placement,
  },
]

export const sourceIntegrationsSteps: Step[] = [
  {
    title: 'Integre seu meio de pagamento',
    content: 'Escolha qual o meio de pagamento utilizado no seu SaaS.',
    target: '#joyride-integrations',
    placement: 'auto' as Placement,
  },
  {
    title: 'Adicione dados manualmente',
    content:
      'Caso não use algum meio de pagamento, você também pode criar seus gráficos manualmente.',
    target: '#joyride-manualmente',
    placement: 'auto' as Placement,
  },
]

export const cadastroCustosSteps: Step[] = [
  {
    title: 'Cadastre seus custos',
    content:
      'Cadastre aqui os custos da sua empresa, isso gerará métricas como CAC, CAC Payback...',
    target: '#joyride-custos',
    placement: 'auto' as Placement,
  },
  {
    title: 'Cadastre seu headcount',
    content:
      'Cadastre aqui o headcount da sua empresa, isso gerará métricas como MRR per Employee, entre outras...',
    target: '#joyride-headcount',
    placement: 'auto' as Placement,
  },
]

I have 4 components on the same page, each one using its own steps with a context for the joyride. I save the steps on my backend and then I push back in my getCurrentUser endpoint.

Anyone has already passed through this?

thealdoamati commented 3 months ago

This is the page that I tried to use it

import { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
import IntegrationsLayout from '../../../../components/Integrations/IntegrationsLayout'
import ManualIntegration from '../../../../components/Integrations/Manual/ManualIntegration'
import AllIntegration from '../../../../components/Integrations/All/AllIntegration'
import SourceIntegration from '../../../../components/Integrations/Sources/SourcesIntegration'
import CadastroCustos from '../../../../components/Integrations/Custos/CadastroCustos'
import ConfigsIntegrations from '../../../../components/Integrations/Configs/ConfigsIntegration'
import StepContextProvider from '../../../../contexts/StepContext'
import { JoyrideProgressProvider } from '../../../../contexts/JoyrideContext'

export default function Integrations() {
  const router = useRouter()
  const [step, setStep] = useState<number>(1)

  useEffect(() => {
    if (router.query.step) {
      const stepFromUrl = Number(router.query.step)
      if (!isNaN(stepFromUrl)) {
        setStep(stepFromUrl)
      }
    }
  }, [router.query.step])

  function handleStep(step: number) {
    switch (step) {
      case 1:
        return <AllIntegration setStep={setStep} />
      case 2:
        return <ManualIntegration />
      case 3:
        return <SourceIntegration setStep={setStep} />
      case 4:
        return <CadastroCustos />
      case 5:
        return <ConfigsIntegrations />
    }
  }
  return (
    <IntegrationsLayout setStep={setStep} step={step}>
      <StepContextProvider>
        <JoyrideProgressProvider>{handleStep(step)}</JoyrideProgressProvider>
      </StepContextProvider>
    </IntegrationsLayout>
  )
}

I created a context to work in 4 different components on this page

import React, { createContext, useContext, useState } from 'react'
import { api } from '../services/api'
import { parseCookies } from 'nookies'

// Definindo o tipo para o contexto do Joyride// Definindo o tipo para o contexto do Joyride
interface JoyrideProgressContextType {
  joyrideProgress: string
  updateProgress: (componentId: string, newStep: number) => void
}

// Criando o contexto com um valor padrão
const JoyrideProgressContext = createContext<JoyrideProgressContextType>({
  joyrideProgress: '11213141',
  updateProgress: (componentId, newStep) => {
    console.warn(
      `UpdateProgress called with componentId: ${componentId} and newStep: ${newStep}`,
    )
  },
})

export const JoyrideProgressProvider = ({ children }: any) => {
  const [joyrideProgress, setJoyrideProgress] = useState('11213141')

  const updateProgress = async (componentId: string, newStep: number) => {
    const index = parseInt(componentId, 10) - 1 // Convert to zero-based index
    const progressArray = joyrideProgress.split('').map(Number)

    // Update the step at the correct index
    progressArray[index] = newStep

    // Convert the array back to a string and then to a number
    const updatedProgress = parseInt(progressArray.join(''), 10)

    // Update local state
    setJoyrideProgress(updatedProgress.toString())

    // Prepare the request
    const { userSessionToken } = parseCookies()
    const config = {
      method: 'put',

      headers: {

      data: { step: updatedProgress }, // Send the updated number as an integer
    }

    // Perform the request
    try {
      const response = await api(config)
      console.log(`Onboarding step updated successfully:`, response.data)
    } catch (error) {
      console.error('Error updating onboarding step:', error)
    }
  }

  return (
    <JoyrideProgressContext.Provider
      value={{ joyrideProgress, updateProgress }}
    >
      {children}
    </JoyrideProgressContext.Provider>
  )
}

export const useJoyrideProgress = () => useContext(JoyrideProgressContext)

This is one of the components that I tried to use and it's bugged on production

import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { parseCookies, destroyCookie } from 'nookies'
import { useRouter } from 'next/router'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import AsaasLogo from '../../../assets/integracoes/AsaasLogo.svg'
import PagarmeLogo from '../../../assets/integracoes/PagarmeLogo.svg'
import IuguLogo from '../../../assets/integracoes/IuguLogo.svg'
import VindiLogo from '../../../assets/integracoes/VindiLogo.svg'
import SuperlogicaLogo from '../../../assets/integracoes/SuperlogicaLogo.svg'
import SheetsLogo from '../../../assets/integracoes/SheetsLogo.svg'
import Stripe from '../../../assets/integracoes/Stripe.svg'
import Engrenagem from '../../../assets/integracoes/engrenagem.svg'
import { formatDistanceToNow, parseISO, isBefore, subDays } from 'date-fns'
import pt from 'date-fns/locale/pt'
import { api } from '../../../services/api'
import { BasicTable } from '../../Utils/Table/BasicTable/BasicTable'
import ReactJoyride from 'react-joyride'
import { allIntegrationsSteps } from '../../../utils/steps/stepsJoyride'
import { useJoyrideProgress } from '../../../contexts/JoyrideContext'
interface Props {
  setStep: Dispatch<SetStateAction<number>>
}

const SUCCESS_MSG = 'Conexão excluída com sucesso!'
const ERROR_MSG = 'Deu ruim'

export default function AllIntegration({ setStep }: Props) {
  const [isPageAllowed, setIsPageAllowed] = useState<boolean>(false)
  const [connections, setConnections] = useState<any>([])
  const [isJoyrideRunning, setIsJoyrideRunning] = useState(false)
  const { joyrideProgress, updateProgress } = useJoyrideProgress()

  const [loadingConnections, setLoadingConnections] = useState<{
    [key: string]: boolean
  }>({})

  const { push } = useRouter()
  const logoMapping = {
    asaas: AsaasLogo,
    stripe: Stripe,
    pagarme: PagarmeLogo,
    iugu: IuguLogo,
    vindi: VindiLogo,
    superlogica: SuperlogicaLogo,
    googleSheets: SheetsLogo,
  }

  async function getUserData() {
    const { userAddress, userSessionToken } = parseCookies() // Obtém o endereço do usuário e o token da sessão do usuário a partir dos cookies.

    if (userSessionToken) {
      const body = JSON.stringify({
        address: userAddress,
      })
      const config = {
        method: 'post',
        url: '/getCurrentUser',

        data: body,
      }
      await api(config)
        .then((res) => {
          setIsPageAllowed(true)
        })
        .catch((err) => {
          console.log('vou puxarrr1')
          localStorage.removeItem('@scalable: user-state-1.0.0')
          localStorage.clear()
          destroyCookie(undefined, 'userAddress')
          destroyCookie(undefined, 'userSessionToken')
          console.log(err)
          push('/')
        })
    }
  }
  console.log('a')
  useEffect(() => {
    getUserData()
    const { userSessionToken } = parseCookies()
    const config = {
      method: 'get',

    api(config)
      .then((response) => {
        if (response && response.data) {
          const updatedConnections = response.data.map((connection: any) => {
            const date = parseISO(connection.atualizadoEm)
            const timeAgo = formatDistanceToNow(date, {
              addSuffix: true,
              locale: pt,
            })
            const isOutdated = isBefore(date, subDays(new Date(), 1))
            return {
              ...connection,
              logo:
                logoMapping[
                  connection.type as keyof {
                    asaas: any
                    stripe: any
                    pagarme: any
                    iugu: any
                    vindi: any
                    superlogica: any
                    googleSheets: any
                  }
                ] || null,
              imgConfig: Engrenagem,
              atualizadoEm: timeAgo,
              isOutdated,
            }
          })
          setConnections(updatedConnections)
        } else {
          toast.error('Dados nulos do backEnd')
        }
      })
      .catch(() => {
        toast.error('Erro! Tente novamente')
      })
  }, [])

  function generateApiConfig(method: string, url: string, data?: any) {
    const { userSessionToken } = parseCookies()
    return {
      method,
      url,

  const updateConnectionMappings: { [key: string]: string } = {
    asaas: '/updateConnectionAsaas',
    pagarme: '/updateConnectionPagarme',
    superlogica: '/updateConnectionSuperlogica',
    stripe: '/updateConnectionStripe',
    iugu: '/updateConnectionIugu',
    vindi: '/updateConnectionVindi',
    googleSheets: '/updateConnectionGoogleSheets',
  }

  const handleUpdateConnection = (
    connectionId: string | number,
    connectionType: string,
  ) => {
    const url = updateConnectionMappings[connectionType]
    if (!url) {
      toast.error('Tipo de conexão não reconhecido.')
      return
    }
    toast.info(
      'Essa atualização pode levar até 20 minutos, mas você pode voltar depois de concluída!',
    )
    setLoadingConnections((prev) => ({ ...prev, [connectionId]: true })) // Start loading

    const config = generateApiConfig('post', url, { id: connectionId })

    api(config)
      .then(() => {
        setLoadingConnections((prev) => ({ ...prev, [connectionId]: false })) // Stop loading
        toast.success('Conexão atualizada com sucesso!')
      })
      .catch((err) => {
        setLoadingConnections((prev) => ({ ...prev, [connectionId]: false })) // Stop loading
        toast.error('Erro ao atualizar a conexão', err)
      })
  }

  const deleteConnectionMappings: { [key: string]: string } = {
    asaas: '/deleteConnectionAsaas',
    pagarme: '/deleteConnectionPagarme',
    stripe: '/deleteConnectionStripe',
    iugu: '/deleteConnectionIugu',
    vindi: '/deleteConnectionVindi',
    superlogica: '/deleteConnectionSuperlogica',
    googleSheets: '/deleteConnectionGoogleSheets',
  }

  const handleDeleteConnection = (
    connectionId: string | number,
    connectionType: string,
  ) => {
    const url = deleteConnectionMappings[connectionType]
    if (!url) {
      toast.error('Tipo de conexão não reconhecido.')
      return
    }

    const config = generateApiConfig('post', url, { id: connectionId })

    api(config)
      .then(() => {
        // Remove a conexão da lista local
        const updatedConnections = connections.filter(
          (conn: any) => conn.id !== connectionId,
        )
        setConnections(updatedConnections)

        toast.success(SUCCESS_MSG)
      })
      .catch((err) => {
        toast.error(ERROR_MSG, err)
      })
  }

  async function getOnboardingStepFromUser() {
    const { userAddress, userSessionToken } = parseCookies()
    const body = JSON.stringify({ address: userAddress })
    const config = {
      method: 'post',
      url: '/getCurrentUser',

      data: body,
    }

    try {
      const response = await api(config)
      let onboardingStep = response.data.onBoardingStepsSaas
      // Garantindo que onboardingStep seja uma string
      onboardingStep = onboardingStep.toString()
      console.log('Current onboarding step from API:', onboardingStep)
      const secondDigit = parseInt(onboardingStep.charAt(1), 10) || 0
      console.log('Second digit from onboarding step:', secondDigit)
      return secondDigit
    } catch (error) {
      console.error('Failed to get onboarding step:', error)
      return 0
    }
  }

  // Use o isRunning baseado no estado do contexto, não no estado local
  const componentId = '2' // Identificador do componente, ajuste conforme necessário
  const currentStep = parseInt(joyrideProgress[parseInt(componentId) - 1])
  const isRunning = currentStep < 2

  function finishOnBoarding() {
    console.log('finishOnBoarding called')
    // Passa os dois argumentos separadamente.
    updateProgress('2', 2) // Aqui estamos assumindo que '2' é o componentId.
  }

  useEffect(() => {
    console.log('isRunning changed:', isRunning)
  }, [isRunning])

  useEffect(() => {
    async function checkOnboardingStep() {
      const step = await getOnboardingStepFromUser()
      const shouldRunJoyride = step !== 2 // Execute o Joyride somente se o segundo dígito não for 2
      setIsJoyrideRunning(shouldRunJoyride)
      console.log('Step obtained from getOnboardingStepFromUser:', step)
      console.log('isJoyrideRunning set to:', shouldRunJoyride)
    }

    checkOnboardingStep()
  }, [])

  if (!isPageAllowed) {
    return <div></div>
  }

  return (
    <>
      <div id="joyride-welcome" className="flex flex-col w-full gap-5">
        <h1 className="text-[#4b4b4b] text-[24px] font-semibold flex items-center justify-start">
          Fonte de dados
        </h1>
        <div className="md:flex grid flex-row items-center gap-5">
          <p className="text-[#000000] text-[16px] font-semibold">
            Suas fontes de dados
          </p>

          <button
            className="bg-[rgba(0,41,255,0.80)] font-semibold rounded-[13px] text-[13px] py-[0.65rem] px-4 text-white hover:bg-blue500 transition duration-500"
            onClick={() => setStep(3)}
          >
            + Adicionar fonte
          </button>
        </div>
        <div id="joyride-docs" className="mt-[20px] md:mt-0">
          <BasicTable
            connections={connections}
            onDelete={handleDeleteConnection}
            onUpdate={handleUpdateConnection}
            setStep={setStep}
            loadingConnections={loadingConnections}
          />
        </div>
      </div>
      <ReactJoyride
        continuous
        hideCloseButton
        scrollToFirstStep
        showProgress
        showSkipButton
        run={isJoyrideRunning}
        steps={allIntegrationsSteps}
        callback={({ status }) => {
          console.log('ReactJoyride status:', status)
          if (status === 'finished' || status === 'skipped') {
            finishOnBoarding()
          }
        }}
        styles={{
          buttonNext: {
            backgroundColor: '#193EFF',
          },
          buttonBack: {
            backgroundColor: 'transparent',
            color: '#193EFF',
          },
        }}
      />
    </>
  )
}
thealdoamati commented 3 months ago

Do you know what can be? @gilbarbara

thealdoamati commented 3 months ago

This is what I see on my inspect elements, the modal is going to the bottom of the page: image