2
0

🪥 Consult submissions

This commit is contained in:
Baptiste Arnaud
2021-12-30 10:24:16 +01:00
parent f088f694b9
commit 1093453c07
25 changed files with 575 additions and 138 deletions

View File

@ -200,3 +200,11 @@ export const UploadIcon = (props: IconProps) => (
<polyline points="16 16 12 12 8 16"></polyline>
</Icon>
)
export const DownloadIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</Icon>
)

View File

@ -0,0 +1,44 @@
import { Checkbox, Flex, Skeleton } from '@chakra-ui/react'
import React from 'react'
type LoadingRowsProps = {
totalColumns: number
}
export const LoadingRows = ({ totalColumns }: LoadingRowsProps) => {
return (
<>
{Array.from(Array(3)).map((row, idx) => (
<Flex as="tr" key={idx}>
<Flex
key={idx}
py={2}
px={4}
border="1px"
as="td"
borderColor="gray.200"
flex="0"
>
<Checkbox isDisabled />
</Flex>
{Array.from(Array(totalColumns)).map((cell, idx) => {
return (
<Flex
key={idx}
py={2}
px={4}
border="1px"
as="td"
borderColor="gray.200"
flex="1"
align="center"
>
<Skeleton height="5px" w="full" />
</Flex>
)
})}
</Flex>
))}
</>
)
}

View File

@ -1,73 +1,110 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-key */
import { Box, Flex } from '@chakra-ui/react'
import { Box, Checkbox, Flex } from '@chakra-ui/react'
import { Answer, Result } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext'
import React from 'react'
import { useTable } from 'react-table'
import React, { useEffect } from 'react'
import { Hooks, useRowSelect, useTable } from 'react-table'
import { parseSubmissionsColumns } from 'services/publicTypebot'
import { parseDateToReadable } from 'services/results'
import { LoadingRows } from './LoadingRows'
// eslint-disable-next-line @typescript-eslint/ban-types
type SubmissionsTableProps = {}
type SubmissionsTableProps = {
results?: (Result & { answers: Answer[] })[]
onNewSelection: (selection: string[]) => void
}
export const SubmissionsTable = ({}: SubmissionsTableProps) => {
export const SubmissionsTable = ({
results,
onNewSelection,
}: SubmissionsTableProps) => {
const { publishedTypebot } = useTypebot()
const columns: any = React.useMemo(
() => parseSubmissionsColumns(publishedTypebot),
[publishedTypebot]
)
const data = React.useMemo(() => [], [])
const { getTableProps, headerGroups, rows, prepareRow, getTableBodyProps } =
useTable({ columns, data })
const data = React.useMemo(
() =>
(results ?? []).map((result) => ({
createdAt: parseDateToReadable(result.createdAt),
...result.answers.reduce(
(o, answer) => ({ ...o, [answer.blockId]: answer.content }),
{}
),
})),
[results]
)
const {
getTableProps,
headerGroups,
rows,
prepareRow,
getTableBodyProps,
selectedFlatRows,
} = useTable({ columns, data }, useRowSelect, checkboxColumnHook) as any
useEffect(() => {
onNewSelection(selectedFlatRows.map((row: any) => row.id))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedFlatRows])
return (
<Flex overflowX="scroll" maxW="full" className="table-wrapper" rounded="md">
<Box as="table" rounded="md" {...getTableProps()}>
<Box as="table" rounded="md" {...getTableProps()} w="full">
<Box as="thead">
{headerGroups.map((headerGroup) => {
{headerGroups.map((headerGroup: any) => {
return (
<Box as="tr" {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => {
<Flex as="tr" {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any, idx: number) => {
return (
<Box
<Flex
py={2}
px={4}
border="1px"
borderColor="gray.200"
as="th"
minW="200px"
color="gray.500"
fontWeight="normal"
textAlign="left"
minW={idx > 0 ? '200px' : 'unset'}
flex={idx > 0 ? '1' : '0'}
{...column.getHeaderProps()}
>
{column.render('Header')}
</Box>
</Flex>
)
})}
</Box>
</Flex>
)
})}
</Box>
<Box as="tbody" {...getTableBodyProps()}>
{rows.map((row) => {
{results === undefined && (
<LoadingRows totalColumns={columns.length} />
)}
{rows.map((row: any) => {
prepareRow(row)
return (
<Box as="tr" {...row.getRowProps()}>
{row.cells.map((cell) => {
<Flex as="tr" {...row.getRowProps()}>
{row.cells.map((cell: any, idx: number) => {
return (
<Box
<Flex
py={2}
px={4}
border="1px"
as="td"
minW="200px"
borderColor="gray.200"
minW={idx > 0 ? '200px' : 'unset'}
{...cell.getCellProps()}
flex={idx > 0 ? '1' : '0'}
>
{cell.render('Cell')}
</Box>
</Flex>
)
})}
</Box>
</Flex>
)
})}
</Box>
@ -75,3 +112,34 @@ export const SubmissionsTable = ({}: SubmissionsTableProps) => {
</Flex>
)
}
const checkboxColumnHook = (hooks: Hooks<any>) => {
hooks.visibleColumns.push((columns) => [
{
id: 'selection',
Header: ({ getToggleAllRowsSelectedProps }: any) => (
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
),
Cell: ({ row }: any) => (
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
),
},
...columns,
])
}
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, checked, ...rest }: any, ref) => {
const defaultRef = React.useRef()
const resolvedRef: any = ref || defaultRef
return (
<Checkbox
ref={resolvedRef}
{...rest}
isIndeterminate={indeterminate}
isChecked={checked}
/>
)
}
)

View File

@ -1,8 +1,17 @@
import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
import {
Button,
Flex,
HStack,
Stack,
Tag,
useToast,
Text,
} from '@chakra-ui/react'
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
import { useTypebot } from 'contexts/TypebotContext'
import { useRouter } from 'next/router'
import React, { useMemo } from 'react'
import { useResultsCount } from 'services/results'
import { AnalyticsContent } from './AnalyticsContent'
import { SubmissionsContent } from './SubmissionContent'
@ -13,6 +22,15 @@ export const ResultsContent = () => {
() => router.pathname.endsWith('analytics'),
[router.pathname]
)
const toast = useToast({
position: 'top-right',
status: 'error',
})
const { totalResults } = useResultsCount({
typebotId: typebot?.id,
onError: (err) => toast({ title: err.name, description: err.message }),
})
return (
<Flex h="full" w="full" justifyContent="center" align="flex-start">
<Stack maxW="1200px" w="full" pt="4" spacing={6}>
@ -24,7 +42,12 @@ export const ResultsContent = () => {
size="sm"
href={`/typebots/${typebot?.id}/results`}
>
Submissions
<Text>Submissions</Text>
{totalResults && (
<Tag size="sm" colorScheme="blue" ml="1">
{totalResults}
</Tag>
)}
</Button>
<Button
as={NextChakraLink}
@ -36,7 +59,15 @@ export const ResultsContent = () => {
Analytics
</Button>
</HStack>
{isAnalytics ? <AnalyticsContent /> : <SubmissionsContent />}
{typebot &&
(isAnalytics ? (
<AnalyticsContent />
) : (
<SubmissionsContent
typebotId={typebot.id}
totalResults={totalResults ?? 0}
/>
))}
</Stack>
</Flex>
)

View File

@ -1,10 +1,75 @@
import {
Button,
HStack,
Stack,
Tag,
useToast,
Text,
Fade,
Flex,
} from '@chakra-ui/react'
import { DownloadIcon, TrashIcon } from 'assets/icons'
import { SubmissionsTable } from 'components/results/SubmissionsTable'
import React from 'react'
import React, { useMemo, useState } from 'react'
import { useResults } from 'services/results'
type Props = { typebotId: string; totalResults: number }
export const SubmissionsContent = ({ typebotId, totalResults }: Props) => {
const [lastResultId, setLastResultId] = useState<string>()
const [selectedIds, setSelectedIds] = useState<string[]>([])
const toast = useToast({
position: 'top-right',
status: 'error',
})
const { results } = useResults({
lastResultId,
typebotId,
onError: (err) => toast({ title: err.name, description: err.message }),
})
const handleNewSelection = (newSelection: string[]) => {
if (newSelection.length === selectedIds.length) return
setSelectedIds(newSelection)
}
const totalSelected = useMemo(
() =>
selectedIds.length === results?.length
? totalResults
: selectedIds.length,
[results?.length, selectedIds.length, totalResults]
)
export const SubmissionsContent = () => {
return (
<>
<SubmissionsTable />
</>
<Stack>
<Flex w="full" justifyContent="flex-end">
<HStack>
<HStack as={Button} colorScheme="blue">
<DownloadIcon />
<Text>Export</Text>
<Fade in={totalSelected > 0} unmountOnExit>
<Tag colorScheme="blue" variant="subtle" size="sm">
{totalSelected}
</Tag>
</Fade>
</HStack>
<Fade in={totalSelected > 0} unmountOnExit>
<HStack as={Button} colorScheme="red">
<TrashIcon />
<Text>Delete</Text>
{totalSelected > 0 && (
<Tag colorScheme="red" variant="subtle" size="sm">
{totalSelected}
</Tag>
)}
</HStack>
</Fade>
</HStack>
</Flex>
<SubmissionsTable results={results} onNewSelection={handleNewSelection} />
</Stack>
)
}

View File

@ -38,6 +38,7 @@
"next-auth": "beta",
"nodemailer": "^6.7.2",
"nprogress": "^0.2.0",
"qs": "^6.10.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-frame-component": "^5.2.1",
@ -59,6 +60,7 @@
"@types/micro-cors": "^0.1.2",
"@types/node": "^16.11.9",
"@types/nprogress": "^0.2.0",
"@types/qs": "^6.9.7",
"@types/react": "^17.0.37",
"@types/react-table": "^7.7.9",
"@types/testing-library__cypress": "^5.0.9",

View File

@ -9,10 +9,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })
const id = req.query.id.toString()
const typebotId = req.query.typebotId.toString()
if (req.method === 'GET') {
const typebot = await prisma.typebot.findUnique({
where: { id },
where: { id: typebotId },
include: {
publishedTypebot: true,
},
@ -23,14 +23,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}
if (req.method === 'DELETE') {
const typebots = await prisma.typebot.delete({
where: { id },
where: { id: typebotId },
})
return res.send({ typebots })
}
if (req.method === 'PUT') {
const data = JSON.parse(req.body)
const typebots = await prisma.typebot.update({
where: { id },
where: { id: typebotId },
data,
})
return res.send({ typebots })
@ -38,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'PATCH') {
const data = JSON.parse(req.body)
const typebots = await prisma.typebot.update({
where: { id },
where: { id: typebotId },
data,
})
return res.send({ typebots })

View File

@ -0,0 +1,39 @@
import { User } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
if (!session?.user)
return res.status(401).send({ message: 'Not authenticated' })
const user = session.user as User
if (req.method === 'GET') {
const typebotId = req.query.typebotId.toString()
const lastResultId = req.query.lastResultId?.toString()
const results = await prisma.result.findMany({
take: 50,
skip: lastResultId ? 1 : 0,
cursor: lastResultId
? {
id: lastResultId,
}
: undefined,
where: {
typebotId,
typebot: { ownerId: user.id },
},
orderBy: {
createdAt: 'desc',
},
include: { answers: true },
})
return res.status(200).send({ results })
}
return methodNotAllowed(res)
}
export default handler

View File

@ -0,0 +1,27 @@
import { User } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
if (!session?.user)
return res.status(401).send({ message: 'Not authenticated' })
const user = session.user as User
if (req.method === 'GET') {
const typebotId = req.query.typebotId.toString()
const totalResults = await prisma.result.count({
where: {
typebotId,
typebot: { ownerId: user.id },
},
})
return res.status(200).send({ totalResults })
}
return methodNotAllowed(res)
}
export default handler

View File

@ -1,6 +1,7 @@
import { DashboardFolder } from '.prisma/client'
import useSWR from 'swr'
import { fetcher, sendRequest } from './utils'
import { stringify } from 'qs'
export const useFolders = ({
parentId,
@ -9,9 +10,7 @@ export const useFolders = ({
parentId?: string
onError: (error: Error) => void
}) => {
const params = new URLSearchParams(
parentId ? { parentId: parentId.toString() } : undefined
)
const params = stringify({ parentId })
const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>(
`/api/folders?${params}`,
fetcher

View File

@ -8,6 +8,9 @@ import {
} from 'bot-engine'
import { sendRequest } from './utils'
import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon } from 'assets/icons'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
export const parseTypebotToPublicTypebot = (
typebot: Typebot
@ -44,12 +47,32 @@ export const updatePublishedTypebot = async (
export const parseSubmissionsColumns = (
typebot?: PublicTypebot
): {
Header: string
Header: JSX.Element
accessor: string
}[] =>
(typebot?.blocks ?? [])
.filter(blockContainsInput)
.map((block) => ({ Header: block.title, accessor: block.id }))
}[] => [
{
Header: (
<HStack>
<CalendarIcon />
<Text>Submitted at</Text>
</HStack>
),
accessor: 'createdAt',
},
...(typebot?.blocks ?? []).filter(blockContainsInput).map((block) => ({
Header: (
<HStack>
<StepIcon
type={
block.steps.find((step) => step.target)?.type ?? StepType.TEXT_INPUT
}
/>
<Text>{block.title}</Text>
</HStack>
),
accessor: block.id,
})),
]
const blockContainsInput = (block: Block) => block.steps.some(stepIsInput)

View File

@ -0,0 +1,60 @@
import { Result } from 'bot-engine'
import useSWR from 'swr'
import { fetcher } from './utils'
import { stringify } from 'qs'
import { Answer } from 'db'
export const useResults = ({
lastResultId,
typebotId,
onError,
}: {
lastResultId?: string
typebotId: string
onError: (error: Error) => void
}) => {
const params = stringify({
lastResultId,
})
const { data, error, mutate } = useSWR<
{ results: (Result & { answers: Answer[] })[] },
Error
>(`/api/typebots/${typebotId}/results?${params}`, fetcher)
if (error) onError(error)
return {
results: data?.results,
isLoading: !error && !data,
mutate,
}
}
export const useResultsCount = ({
typebotId,
onError,
}: {
typebotId?: string
onError: (error: Error) => void
}) => {
const { data, error, mutate } = useSWR<{ totalResults: number }, Error>(
typebotId ? `/api/typebots/${typebotId}/results/count` : null,
fetcher
)
if (error) onError(error)
return {
totalResults: data?.totalResults,
isLoading: !error && !data,
mutate,
}
}
export const parseDateToReadable = (dateStr: string): string => {
const date = new Date(dateStr)
return (
date.toDateString().split(' ').slice(1, 3).join(' ') +
', ' +
date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
)
}

View File

@ -11,6 +11,7 @@ import { Typebot } from 'bot-engine'
import useSWR from 'swr'
import { fetcher, sendRequest, toKebabCase } from './utils'
import { deepEqual } from 'fast-equals'
import { stringify } from 'qs'
export const useTypebots = ({
folderId,
@ -19,9 +20,7 @@ export const useTypebots = ({
folderId?: string
onError: (error: Error) => void
}) => {
const params = new URLSearchParams(
folderId ? { folderId: folderId.toString() } : undefined
)
const params = stringify({ folderId })
const { data, error, mutate } = useSWR<{ typebots: Typebot[] }, Error>(
`/api/typebots?${params}`,
fetcher

View File

@ -1,5 +1,6 @@
import { Answer, PublicTypebot, TypebotViewer } from 'bot-engine'
import React, { useEffect, useState } from 'react'
import { upsertAnswer } from 'services/answer'
import { SEO } from '../components/Seo'
import { createResult, updateResult } from '../services/result'
import { ErrorPage } from './ErrorPage'
@ -38,9 +39,9 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => {
}
}
const handleAnswersUpdate = async (answers: Answer[]) => {
const handleNewAnswer = async (answer: Answer) => {
if (!resultId) return setError(new Error('Result was not created'))
const { error } = await updateResult(resultId, { answers })
const { error } = await upsertAnswer({ ...answer, resultId })
if (error) setError(error)
}
@ -62,7 +63,7 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => {
{resultId && (
<TypebotViewer
typebot={typebot}
onAnswersUpdate={handleAnswersUpdate}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
/>
)}

View File

@ -0,0 +1,25 @@
import { Answer } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'PUT') {
const answer = JSON.parse(req.body) as Answer
const result = await prisma.answer.upsert({
where: {
resultId_blockId_stepId: {
resultId: answer.resultId,
blockId: answer.blockId,
stepId: answer.stepId,
},
},
create: answer,
update: answer,
})
return res.send(result)
}
return methodNotAllowed(res)
}
export default handler

View File

@ -0,0 +1,10 @@
import { Answer } from 'db'
import { sendRequest } from 'utils'
export const upsertAnswer = async (answer: Answer) => {
return sendRequest<Answer>({
url: `/api/answers`,
method: 'PUT',
body: answer,
})
}

View File

@ -16,7 +16,7 @@
},
"devDependencies": {
"dotenv-cli": "^4.1.1",
"turbo": "^1.0.23"
"turbo": "^1.0.24-canary.2"
},
"turbo": {
"baseBranch": "origin/main",

View File

@ -11,18 +11,18 @@ import { deepEqual } from 'fast-equals'
type Props = {
typebot: PublicTypebot
onNewBlockVisible: (blockId: string) => void
onAnswersUpdate: (answers: Answer[]) => void
onNewAnswer: (answer: Answer) => void
onCompleted: () => void
}
export const ConversationContainer = ({
typebot,
onNewBlockVisible,
onAnswersUpdate,
onNewAnswer,
onCompleted,
}: Props) => {
const { document: frameDocument } = useFrame()
const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([])
const [localAnswers, setLocalAnswers] = useState<Answer[]>([])
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
const { answers } = useAnswers()
const bottomAnchor = useRef<HTMLDivElement | null>(null)
@ -44,9 +44,10 @@ export const ConversationContainer = ({
}, [typebot.theme, frameDocument])
useEffect(() => {
if (deepEqual(localAnswers, answers)) return
setLocalAnswers(answers)
onAnswersUpdate(answers)
const answer = [...answers].pop()
if (!answer || deepEqual(localAnswer, answer)) return
setLocalAnswer(answer)
onNewAnswer(answer)
}, [answers])
return (

View File

@ -10,13 +10,13 @@ import { AnswersContext } from '../contexts/AnswersContext'
export type TypebotViewerProps = {
typebot: PublicTypebot
onNewBlockVisible?: (blockId: string) => void
onAnswersUpdate?: (answers: Answer[]) => void
onNewAnswer?: (answer: Answer) => void
onCompleted?: () => void
}
export const TypebotViewer = ({
typebot,
onNewBlockVisible,
onAnswersUpdate,
onNewAnswer,
onCompleted,
}: TypebotViewerProps) => {
const containerBgColor = useMemo(
@ -29,8 +29,8 @@ export const TypebotViewer = ({
const handleNewBlockVisible = (blockId: string) => {
if (onNewBlockVisible) onNewBlockVisible(blockId)
}
const handleAnswersUpdate = (answers: Answer[]) => {
if (onAnswersUpdate) onAnswersUpdate(answers)
const handleNewAnswer = (answer: Answer) => {
if (onNewAnswer) onNewAnswer(answer)
}
const handleCompleted = () => {
if (onCompleted) onCompleted()
@ -60,7 +60,7 @@ export const TypebotViewer = ({
<ConversationContainer
typebot={typebot}
onNewBlockVisible={handleNewBlockVisible}
onAnswersUpdate={handleAnswersUpdate}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
/>
</div>

View File

@ -0,0 +1,3 @@
import { Answer as AnswerFromPrisma } from 'db'
export type Answer = Omit<AnswerFromPrisma, 'resultId'>

View File

@ -1,3 +1,4 @@
export * from './typebot'
export * from './publicTypebot'
export * from './result'
export * from './answer'

View File

@ -1,11 +1,3 @@
import { Result as ResultFromPrisma } from 'db'
export type Result = Omit<ResultFromPrisma, 'answers'> & {
answers: Answer[]
}
export type Answer = {
blockId: string
stepId: string
content: string
}
export type Result = Omit<ResultFromPrisma, 'createdAt'> & { createdAt: string }

View File

@ -0,0 +1,23 @@
/*
Warnings:
- You are about to drop the column `answers` on the `Result` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Result" DROP COLUMN "answers";
-- CreateTable
CREATE TABLE "Answer" (
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"resultId" TEXT NOT NULL,
"stepId" TEXT NOT NULL,
"blockId" TEXT NOT NULL,
"content" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Answer_resultId_blockId_stepId_key" ON "Answer"("resultId", "blockId", "stepId");
-- AddForeignKey
ALTER TABLE "Answer" ADD CONSTRAINT "Answer_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -115,6 +115,17 @@ model Result {
updatedAt DateTime @default(now())
typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Json[]
answers Answer[]
isCompleted Boolean?
}
model Answer {
createdAt DateTime @default(now())
resultId String
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
stepId String
blockId String
content String
@@unique([resultId, blockId, stepId])
}

135
yarn.lock
View File

@ -1482,6 +1482,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
"@types/qs@^6.9.7":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/react-scroll@^1.8.3":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.3.tgz#80951ed1934ab49d4926aad95c26607b8b1a9713"
@ -6046,7 +6051,7 @@ puppeteer@^2.1.1:
rimraf "^2.6.1"
ws "^6.1.0"
qs@^6.6.0:
qs@^6.10.2, qs@^6.6.0:
version "6.10.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.2.tgz#c1431bea37fc5b24c5bdbafa20f16bdf2a4b9ffe"
integrity sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==
@ -7320,83 +7325,83 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
turbo-darwin-64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.23.tgz#0164b1a2ee6782bf2233bb1887b01ac78486378e"
integrity sha512-z6ArzNKpQIw/YhFf20lUGl5VaGTZ94MSppXuuYkCyxMQwK3qye3r2ENQbUKkxfv16fAmfr7z1bJt6YwfOjwPiw==
turbo-darwin-64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24-canary.2.tgz#5d64c95d14fb06f45b2c0c44d1657b1884e009d7"
integrity sha512-MTY6Eea5QQc+ZqC2EqZWtIRaIygenWY7i0f+PVyCyfaXp5TqZiIUljd4s1vfp4tygVg66EVkI2Pu34FsXNVB3g==
turbo-darwin-arm64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.23.tgz#7d6de986595d6ded9d853744c26780b50c7b196c"
integrity sha512-U02Ip7kO7eDN3WRliIrPtqYFqcpsvpNsE5fX1T0uFjXfSJruiDBrrNpGmZEh8hatS5wQyG7VaxW+izJqd7TCsw==
turbo-darwin-arm64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24-canary.2.tgz#2169cebc1be5d8fbcf8334a50d411432c7480c10"
integrity sha512-FaaAJ25smSeKjnkyUYxJhth0L59VmGjM9MrHXg+KhjU9jPFzyXTr9Bnb0nV6TAFRcL+IzZ3TXHykRCqfY4FjYA==
turbo-freebsd-64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.23.tgz#f78d2056414496334cb8c6bc28ac7f3d5d0ab30c"
integrity sha512-5XT8pp0uJ1vRqNJydacn6ouY4COUeufOdLuiPOWAOsvMW5FOJNAOdAJ7lISBBqhCpYF8CEZ9kCAI2f9nOzzY0Q==
turbo-freebsd-64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24-canary.2.tgz#91528c2a75f621d7e7861e94b73c9a70443206ef"
integrity sha512-alNrWyWvpIdKoQARB85PJchwr6VqKbWdayq2zlzGnSnTmH0EQb0bSDL9zNsdiKu4ISGE2E7YUN616lfvqx3wHA==
turbo-freebsd-arm64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.23.tgz#d7d4eca48a21da17403c804e5bd52a3daffbdc1c"
integrity sha512-70BGJTVX3jYvzDISp8dWM/rwiK4CDlNVQJnnlUMKrueFtpgHZhsRKjIeNgIKxVSTyp4xT0Vl4xhc9zFhMT17aw==
turbo-freebsd-arm64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24-canary.2.tgz#8cea1bf6335e284beea3136eb7cc811da8501c72"
integrity sha512-vfS+L5NqRLN2w4R0QgmBFl9Z9pXnIYwoS3IWcfx2SRqTUdcqfrM3Mq3jHUdfQE2DufMYcGt1u28aakYb5LEpdA==
turbo-linux-32@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.23.tgz#c11de3c98944d43b9e25582c03ddd8f38d6cd85c"
integrity sha512-ee3N8f7h1qg5noKII5Tq2gyG6PVFl/apllX0TPbZiUab6553QPfVt+tB+5W6BZmKGUdHHCvW42xuig0krrrRGw==
turbo-linux-32@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24-canary.2.tgz#41d68a780b805111a05a2e4b692b3f10c944bb51"
integrity sha512-kjhHH3IQv83MrxpW5ScWlC08XzoWgoI2IbrZsZeZYXVdzDgIk5Uf8PfiUe0fJXpdUL8bIFq3nQ+yX8U/FuChAw==
turbo-linux-64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.23.tgz#7700a5e5ad96e741405957e7cd33a7422c49bfbd"
integrity sha512-j2+1P2+rdOXb/yzz78SCmxXLdFSSlCzZS0Psg1vetYozuoT0k+xu7gr6iqEHdBWw39jNFuT72hVqrNZ4TClHcA==
turbo-linux-64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24-canary.2.tgz#18331dfc7b1d0f89a5f82cc17383571186779e85"
integrity sha512-v8+u+gV01UsCl7psvptL+mr863lrVr3L1j0CFKXhjyZQJlEgXoRbkxU4VUWaXwf9ABaadsPsmv9B2oQnudypeA==
turbo-linux-arm64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.23.tgz#79b49e14ef3038f99036134abcf53c17645b5eb9"
integrity sha512-VNW8yyBG63MGUzZ/Y2DS883P8rzz/hQuiCklw2lTBQ4O9dp2FxYrAVnmCxbnsIDZG/wEupdvaHVgqH7j2DJgwg==
turbo-linux-arm64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24-canary.2.tgz#8873aa5b812f668934d16b69d59f5030b3d9fc8e"
integrity sha512-v3/XDIQ9Cyz1dfPXnuJyH2V4xIDGPw9SaQS3FPiGH6TvUULkFYphg+cLttk7DPo3573GDxUQug3c5TvlIZFO8Q==
turbo-linux-arm@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.23.tgz#7140b73572948a0285395feb3abe8a9067f74d68"
integrity sha512-+aD+v03bpkmuXEfk14w2fLH5cAyfyGlAs5gBwQmwyF3XllJ40OL/cw9xDnBREpZsnm9o1GTQpr0wmepjw9d4iQ==
turbo-linux-arm@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24-canary.2.tgz#22126953b881f952a1b5f480a3053c514cf314aa"
integrity sha512-pkfSuuOjN8DDJDOAdx0jqwTL4jeEsZbjmgNnhgrkb1HwVjQCjmqHbh15Wa2E51G12/Ctr6h2aKit2kbLXPfGWw==
turbo-linux-mips64le@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.23.tgz#fa4e989a8e149e3684f4732ed1b2a386fc829c8a"
integrity sha512-CJNd7F9z8EhqcdJnd1rSiU/+hSNG3E+fudQ5pr55y0nhSShFMMdZxdLvL5zzhWpBl1g8YldphjPqNLChgGatKw==
turbo-linux-mips64le@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24-canary.2.tgz#11254292c40ac8acef8452a379bb74bad39bf858"
integrity sha512-35WJMoIxfIG8ruoMJVdOCxuQlJjNeLbxbqigDizJh2zkjlJITzEK+WwmF9KrqzMaN7Ks4LPq+xd/YvBEULx4Zw==
turbo-linux-ppc64le@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.23.tgz#3c1e0f5a79b8c34d790c7828ec443ed73ed95706"
integrity sha512-RjzDqXQl+nqV+2A6MRk1sALBQtpCB/h8qDWhJIL/VUgToTMdllAThiYI+lALLmGWBumXs4XDbk+83FGJlJ1bDg==
turbo-linux-ppc64le@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24-canary.2.tgz#abc93ede90deca66f20c23fedc0f2b032e43a48a"
integrity sha512-ZCipAVam+ZKyiQ5KYrL8pPgQ06JJ09H6mqTxEAP0OP2kN6Se+TuycFjAsedE0Gn/G+6MBmStjR99ha+I2fRnDA==
turbo-windows-32@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.23.tgz#17d58f43406cd68062131924f94c2df8ca159f9e"
integrity sha512-qTfldTd5OO/s8unGFnITe4IX8ua2SNj/VVQbvVcObDJK6xrfBY4tDS78rIo9//xTSRyK+28AoPxRZsVqqF2hlA==
turbo-windows-32@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24-canary.2.tgz#c24ae090c84c4ad83833deaeb0dba68186fe8e7d"
integrity sha512-b9FBMoxhunGQNN/DwT9KK7g4FwDG3/cc4DEV5TdD809I66LmyVk1ThvpnUdZIRZRymcWq1c6kjT+0LfJFEBxGw==
turbo-windows-64@1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.23.tgz#0e53fde0395fa88006be7fcd36c1853995cfa319"
integrity sha512-yzhF6GEYiyrXoO/l9T6amB16OITsFXdeMTc0Vf2BZFOG1fn9Muc4CxQ+CcdsLlai+TqQTKlpEf0+l83vI+R2Jw==
turbo-windows-64@1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24-canary.2.tgz#f783fcb3661634cc3c1f578fdc79441ed3de5f8e"
integrity sha512-HqXW3k+5h4nP8eCg5i/0NhUlR4+3vxTSyI3UvoACVz/IkiUIwVKF9ZhlAfFpkRCYU+1KKOMs7iDWlYiTyW4Inw==
turbo@^1.0.23:
version "1.0.23"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.23.tgz#9a8f54f116daee67e0c131c4c936f075d32281a2"
integrity sha512-/VHsARQyl8wsfCJtuJicjYq7Do/P5yjJde3bjG9TAZHj/PzA9LuRU7WOEPfUrT+felOQ3vwUMZtC8xrYFmndLA==
turbo@^1.0.24-canary.2:
version "1.0.24-canary.2"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24-canary.2.tgz#7b7c134d4e6dce0cf6e9e7b09393dde9767c397e"
integrity sha512-Ve/KYeSxPBGKB6qmHc7BBZhZ7AaFH6WeUjd97IB38zfEtrcLlQ0xziY8EX7qiACarbrV9TKkYG5QctI5OIbD9A==
optionalDependencies:
turbo-darwin-64 "1.0.23"
turbo-darwin-arm64 "1.0.23"
turbo-freebsd-64 "1.0.23"
turbo-freebsd-arm64 "1.0.23"
turbo-linux-32 "1.0.23"
turbo-linux-64 "1.0.23"
turbo-linux-arm "1.0.23"
turbo-linux-arm64 "1.0.23"
turbo-linux-mips64le "1.0.23"
turbo-linux-ppc64le "1.0.23"
turbo-windows-32 "1.0.23"
turbo-windows-64 "1.0.23"
turbo-darwin-64 "1.0.24-canary.2"
turbo-darwin-arm64 "1.0.24-canary.2"
turbo-freebsd-64 "1.0.24-canary.2"
turbo-freebsd-arm64 "1.0.24-canary.2"
turbo-linux-32 "1.0.24-canary.2"
turbo-linux-64 "1.0.24-canary.2"
turbo-linux-arm "1.0.24-canary.2"
turbo-linux-arm64 "1.0.24-canary.2"
turbo-linux-mips64le "1.0.24-canary.2"
turbo-linux-ppc64le "1.0.24-canary.2"
turbo-windows-32 "1.0.24-canary.2"
turbo-windows-64 "1.0.24-canary.2"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"