Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number = 300): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay)
|
||||
return () => clearTimeout(timer)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number = 300): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay)
|
||||
return () => clearTimeout(timer)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
@@ -1,130 +1,130 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useCallback, useState } from 'react'
|
||||
|
||||
export interface VoteUpdate {
|
||||
projectId: string
|
||||
totalVotes: number
|
||||
averageScore: number | null
|
||||
latestVote: { score: number; isAudienceVote: boolean; votedAt: string } | null
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface AudienceVoteUpdate {
|
||||
projectId: string
|
||||
audienceVotes: number
|
||||
audienceAverage: number | null
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SessionStatusUpdate {
|
||||
status: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface ProjectChangeUpdate {
|
||||
projectId: string | null
|
||||
projectIndex: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
interface SSECallbacks {
|
||||
onVoteUpdate?: (data: VoteUpdate) => void
|
||||
onAudienceVote?: (data: AudienceVoteUpdate) => void
|
||||
onSessionStatus?: (data: SessionStatusUpdate) => void
|
||||
onProjectChange?: (data: ProjectChangeUpdate) => void
|
||||
onConnected?: () => void
|
||||
onError?: (error: Event) => void
|
||||
}
|
||||
|
||||
export function useLiveVotingSSE(
|
||||
sessionId: string | null,
|
||||
callbacks: SSECallbacks
|
||||
) {
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const callbacksRef = useRef(callbacks)
|
||||
callbacksRef.current = callbacks
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!sessionId) return
|
||||
|
||||
// Close any existing connection
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
const url = `${baseUrl}/api/live-voting/stream?sessionId=${sessionId}`
|
||||
const es = new EventSource(url)
|
||||
eventSourceRef.current = es
|
||||
|
||||
es.addEventListener('connected', () => {
|
||||
setIsConnected(true)
|
||||
callbacksRef.current.onConnected?.()
|
||||
})
|
||||
|
||||
es.addEventListener('vote_update', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as VoteUpdate
|
||||
callbacksRef.current.onVoteUpdate?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('audience_vote', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as AudienceVoteUpdate
|
||||
callbacksRef.current.onAudienceVote?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('session_status', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as SessionStatusUpdate
|
||||
callbacksRef.current.onSessionStatus?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('project_change', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as ProjectChangeUpdate
|
||||
callbacksRef.current.onProjectChange?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.onerror = (event) => {
|
||||
setIsConnected(false)
|
||||
callbacksRef.current.onError?.(event)
|
||||
|
||||
// Auto-reconnect after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (eventSourceRef.current === es) {
|
||||
connect()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}, [sessionId])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
setIsConnected(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
return () => disconnect()
|
||||
}, [connect, disconnect])
|
||||
|
||||
return { isConnected, reconnect: connect, disconnect }
|
||||
}
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useCallback, useState } from 'react'
|
||||
|
||||
export interface VoteUpdate {
|
||||
projectId: string
|
||||
totalVotes: number
|
||||
averageScore: number | null
|
||||
latestVote: { score: number; isAudienceVote: boolean; votedAt: string } | null
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface AudienceVoteUpdate {
|
||||
projectId: string
|
||||
audienceVotes: number
|
||||
audienceAverage: number | null
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SessionStatusUpdate {
|
||||
status: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface ProjectChangeUpdate {
|
||||
projectId: string | null
|
||||
projectIndex: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
interface SSECallbacks {
|
||||
onVoteUpdate?: (data: VoteUpdate) => void
|
||||
onAudienceVote?: (data: AudienceVoteUpdate) => void
|
||||
onSessionStatus?: (data: SessionStatusUpdate) => void
|
||||
onProjectChange?: (data: ProjectChangeUpdate) => void
|
||||
onConnected?: () => void
|
||||
onError?: (error: Event) => void
|
||||
}
|
||||
|
||||
export function useLiveVotingSSE(
|
||||
sessionId: string | null,
|
||||
callbacks: SSECallbacks
|
||||
) {
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const callbacksRef = useRef(callbacks)
|
||||
callbacksRef.current = callbacks
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!sessionId) return
|
||||
|
||||
// Close any existing connection
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
const url = `${baseUrl}/api/live-voting/stream?sessionId=${sessionId}`
|
||||
const es = new EventSource(url)
|
||||
eventSourceRef.current = es
|
||||
|
||||
es.addEventListener('connected', () => {
|
||||
setIsConnected(true)
|
||||
callbacksRef.current.onConnected?.()
|
||||
})
|
||||
|
||||
es.addEventListener('vote_update', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as VoteUpdate
|
||||
callbacksRef.current.onVoteUpdate?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('audience_vote', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as AudienceVoteUpdate
|
||||
callbacksRef.current.onAudienceVote?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('session_status', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as SessionStatusUpdate
|
||||
callbacksRef.current.onSessionStatus?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('project_change', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as ProjectChangeUpdate
|
||||
callbacksRef.current.onProjectChange?.(data)
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
})
|
||||
|
||||
es.onerror = (event) => {
|
||||
setIsConnected(false)
|
||||
callbacksRef.current.onError?.(event)
|
||||
|
||||
// Auto-reconnect after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (eventSourceRef.current === es) {
|
||||
connect()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}, [sessionId])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
setIsConnected(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
return () => disconnect()
|
||||
}, [connect, disconnect])
|
||||
|
||||
return { isConnected, reconnect: connect, disconnect }
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function usePipelineInlineEdit(pipelineId: string) {
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
const updateMutation = trpc.pipeline.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.pipeline.getDraft.invalidate({ id: pipelineId })
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`Failed to update pipeline: ${err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const updateConfigMutation = trpc.stage.updateConfig.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.pipeline.getDraft.invalidate({ id: pipelineId })
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`Failed to update stage: ${err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const updatePipeline = async (
|
||||
data: { name?: string; slug?: string; status?: 'DRAFT' | 'ACTIVE' | 'CLOSED' | 'ARCHIVED'; settingsJson?: Record<string, unknown> }
|
||||
) => {
|
||||
await updateMutation.mutateAsync({ id: pipelineId, ...data })
|
||||
toast.success('Pipeline updated')
|
||||
}
|
||||
|
||||
const updateStageConfig = async (
|
||||
stageId: string,
|
||||
configJson: Record<string, unknown>,
|
||||
name?: string
|
||||
) => {
|
||||
await updateConfigMutation.mutateAsync({ id: stageId, configJson, name })
|
||||
toast.success('Stage configuration updated')
|
||||
}
|
||||
|
||||
return {
|
||||
isUpdating: updateMutation.isPending || updateConfigMutation.isPending,
|
||||
updatePipeline,
|
||||
updateStageConfig,
|
||||
}
|
||||
}
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function usePipelineInlineEdit(pipelineId: string) {
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
const updateMutation = trpc.pipeline.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.pipeline.getDraft.invalidate({ id: pipelineId })
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`Failed to update pipeline: ${err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const updateConfigMutation = trpc.stage.updateConfig.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.pipeline.getDraft.invalidate({ id: pipelineId })
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`Failed to update stage: ${err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const updatePipeline = async (
|
||||
data: { name?: string; slug?: string; status?: 'DRAFT' | 'ACTIVE' | 'CLOSED' | 'ARCHIVED'; settingsJson?: Record<string, unknown> }
|
||||
) => {
|
||||
await updateMutation.mutateAsync({ id: pipelineId, ...data })
|
||||
toast.success('Pipeline updated')
|
||||
}
|
||||
|
||||
const updateStageConfig = async (
|
||||
stageId: string,
|
||||
configJson: Record<string, unknown>,
|
||||
name?: string
|
||||
) => {
|
||||
await updateConfigMutation.mutateAsync({ id: stageId, configJson, name })
|
||||
toast.success('Stage configuration updated')
|
||||
}
|
||||
|
||||
return {
|
||||
isUpdating: updateMutation.isPending || updateConfigMutation.isPending,
|
||||
updatePipeline,
|
||||
updateStageConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,153 +1,153 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
interface StageliveSseState {
|
||||
isConnected: boolean
|
||||
activeProject: {
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
description: string | null
|
||||
} | null
|
||||
openCohorts: Array<{
|
||||
id: string
|
||||
name: string
|
||||
isOpen: boolean
|
||||
projectIds: string[]
|
||||
}>
|
||||
isPaused: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
interface UseStageliveSseOptions {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useStageliveSse(
|
||||
sessionId: string | null,
|
||||
options: UseStageliveSseOptions = {}
|
||||
) {
|
||||
const { enabled = true } = options
|
||||
const [state, setState] = useState<StageliveSseState>({
|
||||
isConnected: false,
|
||||
activeProject: null,
|
||||
openCohorts: [],
|
||||
isPaused: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const retryCountRef = useRef(0)
|
||||
const maxRetries = 10
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!sessionId || !enabled) return
|
||||
|
||||
// Clean up existing connection
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
const url = `/api/sse/stage-live/${sessionId}`
|
||||
const es = new EventSource(url)
|
||||
eventSourceRef.current = es
|
||||
|
||||
es.onopen = () => {
|
||||
setState((prev) => ({ ...prev, isConnected: true, error: null }))
|
||||
retryCountRef.current = 0
|
||||
}
|
||||
|
||||
es.addEventListener('cursor.updated', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
activeProject: data.activeProject ?? prev.activeProject,
|
||||
isPaused: data.isPaused ?? prev.isPaused,
|
||||
}))
|
||||
} catch {
|
||||
// Ignore malformed events
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('cohort.window.changed', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
openCohorts: data.cohorts ?? prev.openCohorts,
|
||||
}))
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('vote.received', () => {
|
||||
// Used for UI feedback (e.g. flash animation)
|
||||
// No state change needed
|
||||
})
|
||||
|
||||
es.addEventListener('session.paused', () => {
|
||||
setState((prev) => ({ ...prev, isPaused: true }))
|
||||
})
|
||||
|
||||
es.addEventListener('session.resumed', () => {
|
||||
setState((prev) => ({ ...prev, isPaused: false }))
|
||||
})
|
||||
|
||||
es.addEventListener('init', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
setState({
|
||||
isConnected: true,
|
||||
activeProject: data.activeProject ?? null,
|
||||
openCohorts: data.openCohorts ?? [],
|
||||
isPaused: data.isPaused ?? false,
|
||||
error: null,
|
||||
})
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
|
||||
es.onerror = () => {
|
||||
es.close()
|
||||
setState((prev) => ({ ...prev, isConnected: false }))
|
||||
|
||||
if (retryCountRef.current < maxRetries) {
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCountRef.current), 30000)
|
||||
retryCountRef.current++
|
||||
reconnectTimeoutRef.current = setTimeout(connect, delay)
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: 'Connection lost. Please refresh the page.',
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [sessionId, enabled])
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [connect])
|
||||
|
||||
const reconnect = useCallback(() => {
|
||||
retryCountRef.current = 0
|
||||
connect()
|
||||
}, [connect])
|
||||
|
||||
return { ...state, reconnect }
|
||||
}
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
interface StageliveSseState {
|
||||
isConnected: boolean
|
||||
activeProject: {
|
||||
id: string
|
||||
title: string
|
||||
teamName: string | null
|
||||
description: string | null
|
||||
} | null
|
||||
openCohorts: Array<{
|
||||
id: string
|
||||
name: string
|
||||
isOpen: boolean
|
||||
projectIds: string[]
|
||||
}>
|
||||
isPaused: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
interface UseStageliveSseOptions {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useStageliveSse(
|
||||
sessionId: string | null,
|
||||
options: UseStageliveSseOptions = {}
|
||||
) {
|
||||
const { enabled = true } = options
|
||||
const [state, setState] = useState<StageliveSseState>({
|
||||
isConnected: false,
|
||||
activeProject: null,
|
||||
openCohorts: [],
|
||||
isPaused: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const retryCountRef = useRef(0)
|
||||
const maxRetries = 10
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!sessionId || !enabled) return
|
||||
|
||||
// Clean up existing connection
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
const url = `/api/sse/stage-live/${sessionId}`
|
||||
const es = new EventSource(url)
|
||||
eventSourceRef.current = es
|
||||
|
||||
es.onopen = () => {
|
||||
setState((prev) => ({ ...prev, isConnected: true, error: null }))
|
||||
retryCountRef.current = 0
|
||||
}
|
||||
|
||||
es.addEventListener('cursor.updated', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
activeProject: data.activeProject ?? prev.activeProject,
|
||||
isPaused: data.isPaused ?? prev.isPaused,
|
||||
}))
|
||||
} catch {
|
||||
// Ignore malformed events
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('cohort.window.changed', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
openCohorts: data.cohorts ?? prev.openCohorts,
|
||||
}))
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('vote.received', () => {
|
||||
// Used for UI feedback (e.g. flash animation)
|
||||
// No state change needed
|
||||
})
|
||||
|
||||
es.addEventListener('session.paused', () => {
|
||||
setState((prev) => ({ ...prev, isPaused: true }))
|
||||
})
|
||||
|
||||
es.addEventListener('session.resumed', () => {
|
||||
setState((prev) => ({ ...prev, isPaused: false }))
|
||||
})
|
||||
|
||||
es.addEventListener('init', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
setState({
|
||||
isConnected: true,
|
||||
activeProject: data.activeProject ?? null,
|
||||
openCohorts: data.openCohorts ?? [],
|
||||
isPaused: data.isPaused ?? false,
|
||||
error: null,
|
||||
})
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
|
||||
es.onerror = () => {
|
||||
es.close()
|
||||
setState((prev) => ({ ...prev, isConnected: false }))
|
||||
|
||||
if (retryCountRef.current < maxRetries) {
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCountRef.current), 30000)
|
||||
retryCountRef.current++
|
||||
reconnectTimeoutRef.current = setTimeout(connect, delay)
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: 'Connection lost. Please refresh the page.',
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [sessionId, enabled])
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [connect])
|
||||
|
||||
const reconnect = useCallback(() => {
|
||||
retryCountRef.current = 0
|
||||
connect()
|
||||
}, [connect])
|
||||
|
||||
return { ...state, reconnect }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user