app-tutor-ai-consumer 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/staging-staging.yml +148 -0
- package/.github/workflows/staging.yml +1 -2
- package/CHANGELOG.md +13 -0
- package/config/rspack/rspack.config.js +5 -1
- package/config/vitest/__mocks__/icons.tsx +3 -0
- package/config/vitest/__mocks__/intersection-observer.ts +10 -0
- package/config/vitest/__mocks__/sparkie.tsx +2 -11
- package/config/vitest/__mocks__/use-init-sparkie.tsx +14 -0
- package/config/vitest/vitest.config.mts +13 -8
- package/environments/.env.test +2 -0
- package/package.json +3 -3
- package/public/index.html +3 -4
- package/src/config/styles/global.css +2 -2
- package/src/config/tanstack/query-client.ts +2 -1
- package/src/config/tests/utils.tsx +3 -2
- package/src/config/tests/wrappers.tsx +4 -1
- package/src/index.tsx +22 -0
- package/src/lib/components/icons/arrow-down.svg +5 -0
- package/src/lib/components/icons/chevron-down.svg +4 -0
- package/src/lib/components/icons/icon-names.d.ts +1 -1
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +7 -9
- package/src/lib/hooks/index.ts +3 -0
- package/src/lib/hooks/use-intersection-observer-reverse-scroll/index.ts +2 -0
- package/src/lib/hooks/use-intersection-observer-reverse-scroll/use-intersection-observer-reverse-scroll.tsx +147 -0
- package/src/lib/hooks/use-ref-client-height/index.ts +2 -0
- package/src/lib/hooks/use-ref-client-height/use-ref-client-height.tsx +38 -0
- package/src/lib/hooks/use-scroll-to-ref/index.ts +2 -0
- package/src/lib/hooks/use-scroll-to-ref/use-scroll-to-ref.tsx +14 -0
- package/src/lib/hooks/use-throttle/index.ts +3 -0
- package/src/lib/hooks/use-throttle/types.ts +13 -0
- package/src/lib/hooks/use-throttle/use-throttle.spec.tsx +296 -0
- package/src/lib/hooks/use-throttle/use-throttle.tsx +91 -0
- package/src/main/main.spec.tsx +9 -0
- package/src/modules/cursor/__tests__/icursor-update.builder.ts +42 -0
- package/src/modules/cursor/hooks/index.ts +1 -0
- package/src/modules/cursor/hooks/use-update-cursor/index.ts +2 -0
- package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.spec.tsx +23 -0
- package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.ts +11 -0
- package/src/modules/cursor/index.ts +2 -0
- package/src/modules/cursor/service.ts +15 -0
- package/src/modules/cursor/types.ts +9 -0
- package/src/modules/global-providers/index.ts +1 -0
- package/src/modules/messages/__tests__/parsed-message.builder.ts +164 -0
- package/src/modules/messages/components/message-item/message-item.spec.tsx +2 -2
- package/src/modules/messages/components/message-item/message-item.tsx +14 -1
- package/src/modules/messages/components/message-item-end-of-scroll/index.ts +2 -0
- package/src/modules/messages/components/message-item-end-of-scroll/message-item-end-of-scroll.tsx +14 -0
- package/src/modules/messages/components/message-item-error/index.ts +2 -0
- package/src/modules/messages/components/message-item-error/message-item-error.tsx +25 -0
- package/src/modules/messages/components/message-item-loading/index.ts +2 -0
- package/src/modules/messages/components/message-item-loading/message-item-loading.tsx +16 -0
- package/src/modules/messages/components/messages-list/index.ts +1 -1
- package/src/modules/messages/components/messages-list/messages-list.tsx +69 -39
- package/src/modules/messages/constants.ts +1 -0
- package/src/modules/messages/hooks/index.ts +3 -0
- package/src/modules/messages/hooks/use-all-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +30 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +58 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +97 -0
- package/src/modules/messages/hooks/use-manage-scroll/index.ts +2 -0
- package/src/modules/messages/hooks/use-manage-scroll/use-manage-scroll.tsx +66 -0
- package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.spec.tsx +58 -0
- package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.ts +30 -0
- package/src/modules/messages/utils/has-to-update-cursor/index.ts +2 -0
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +33 -0
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +6 -0
- package/src/modules/widget/components/chat-page/chat-page.spec.tsx +28 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +1 -3
- package/src/modules/widget/components/container/container.tsx +20 -14
- package/src/modules/widget/components/index.ts +1 -0
- package/src/modules/widget/components/scroll-to-bottom-button/index.ts +2 -0
- package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +32 -0
- package/src/modules/widget/events.ts +4 -0
- package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +8 -6
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-container-intrinsic-height.atom.ts +13 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react'
|
|
2
|
+
import type { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
|
|
3
|
+
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
|
|
5
|
+
import { formatTime } from '@/src/config/dayjs'
|
|
6
|
+
import { SparkieService } from '@/src/modules/sparkie'
|
|
7
|
+
import { MessagesService } from '../..'
|
|
8
|
+
import { MSG_MAX_COUNT } from '../../constants'
|
|
9
|
+
import type { FetchMessagesResponse, IMessage, ParsedMessage } from '../../types'
|
|
10
|
+
import { messagesParser } from '../../utils'
|
|
11
|
+
import type { UseFetchMessagesProps } from '../use-fetch-messages'
|
|
12
|
+
|
|
13
|
+
export type UseGetMessagesQueryProps = {
|
|
14
|
+
conversationId: string
|
|
15
|
+
profileId: string
|
|
16
|
+
enabled?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const getMessagesInfiniteQuery = ({
|
|
20
|
+
conversationId,
|
|
21
|
+
profileId,
|
|
22
|
+
enabled
|
|
23
|
+
}: UseGetMessagesQueryProps) =>
|
|
24
|
+
({
|
|
25
|
+
queryKey: ['sparkie:messageService:getAll', conversationId, profileId],
|
|
26
|
+
queryFn: async ({ pageParam }: { pageParam?: number }) => {
|
|
27
|
+
const messages = await MessagesService.getMessages({ conversationId, before: pageParam })
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
messages,
|
|
31
|
+
hasMore: messages.length === MSG_MAX_COUNT
|
|
32
|
+
} as FetchMessagesResponse
|
|
33
|
+
},
|
|
34
|
+
initialPageParam: undefined,
|
|
35
|
+
getNextPageParam: (lastPage: FetchMessagesResponse) => {
|
|
36
|
+
if (!lastPage.hasMore) return undefined
|
|
37
|
+
|
|
38
|
+
const minSentAt = Math.min(...lastPage.messages.map((msg) => msg.sentAt))
|
|
39
|
+
|
|
40
|
+
return minSentAt
|
|
41
|
+
},
|
|
42
|
+
enabled,
|
|
43
|
+
select(data) {
|
|
44
|
+
const messages = data.pages?.flatMap((page) => page.messages) ?? []
|
|
45
|
+
return messagesParser({ messages, profileId }).reduce((messagesMap, currentMessage) => {
|
|
46
|
+
const timestamp = formatTime(currentMessage.timestamp, true)
|
|
47
|
+
|
|
48
|
+
if (!messagesMap.has(timestamp)) {
|
|
49
|
+
messagesMap.set(timestamp, [currentMessage])
|
|
50
|
+
return messagesMap
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const existingTimestampValues = Array.from(messagesMap.get(timestamp) ?? [])
|
|
54
|
+
|
|
55
|
+
messagesMap.set(
|
|
56
|
+
timestamp,
|
|
57
|
+
[...existingTimestampValues, currentMessage].sort((a, b) => a.timestamp - b.timestamp)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return messagesMap
|
|
61
|
+
}, new Map<string, ParsedMessage[]>())
|
|
62
|
+
}
|
|
63
|
+
}) as UndefinedInitialDataInfiniteOptions<
|
|
64
|
+
FetchMessagesResponse,
|
|
65
|
+
Error,
|
|
66
|
+
Map<string, ParsedMessage[]>
|
|
67
|
+
>
|
|
68
|
+
|
|
69
|
+
function useInfiniteGetMessages({
|
|
70
|
+
conversationId,
|
|
71
|
+
profileId,
|
|
72
|
+
enabled = false
|
|
73
|
+
}: Omit<UseFetchMessagesProps, 'currentMessages' | 'loadFirstPage'>) {
|
|
74
|
+
const queryClient = useQueryClient()
|
|
75
|
+
const query = getMessagesInfiniteQuery({ conversationId, profileId, enabled })
|
|
76
|
+
|
|
77
|
+
const messageReceived = useCallback(
|
|
78
|
+
(data: IMessage) => {
|
|
79
|
+
if (data.conversationId !== conversationId) return
|
|
80
|
+
|
|
81
|
+
void queryClient.invalidateQueries({ queryKey: query.queryKey })
|
|
82
|
+
},
|
|
83
|
+
[conversationId, queryClient, query.queryKey]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
SparkieService.subscribeEvents({ messageReceived })
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
SparkieService.removeEventSubscription({ messageReceived })
|
|
91
|
+
}
|
|
92
|
+
}, [messageReceived])
|
|
93
|
+
|
|
94
|
+
return useInfiniteQuery(query)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default useInfiniteGetMessages
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import type { RefObject } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useAllMessages } from '../use-all-messages'
|
|
5
|
+
|
|
6
|
+
const threshold = 250 // min scroll pos to show scroller button
|
|
7
|
+
|
|
8
|
+
function useManageScroll<T extends HTMLElement>(scrollerRef: RefObject<T | null>) {
|
|
9
|
+
const { messagesQuery, hasMessages } = useAllMessages()
|
|
10
|
+
const [heightBeforeRender, setHeightBeforeRender] = useState(0)
|
|
11
|
+
const [showScrollButton, setShowScrollButton] = useState(false)
|
|
12
|
+
|
|
13
|
+
const handleScroll = useCallback(() => {
|
|
14
|
+
const scroller = scrollerRef.current
|
|
15
|
+
|
|
16
|
+
if (!scroller) return
|
|
17
|
+
|
|
18
|
+
if (messagesQuery.hasNextPage && scroller && scroller.scrollTop < 50) {
|
|
19
|
+
setHeightBeforeRender(scroller.scrollHeight)
|
|
20
|
+
void messagesQuery.fetchNextPage()
|
|
21
|
+
}
|
|
22
|
+
}, [messagesQuery, scrollerRef])
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const scroller = scrollerRef.current
|
|
26
|
+
|
|
27
|
+
if (!scroller || !hasMessages || !messagesQuery.isSuccess) return
|
|
28
|
+
|
|
29
|
+
scroller.addEventListener('scrollend', handleScroll)
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
scroller.removeEventListener('scrollend', handleScroll)
|
|
33
|
+
}
|
|
34
|
+
}, [handleScroll, hasMessages, messagesQuery.isSuccess, scrollerRef])
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const scroller = scrollerRef.current
|
|
38
|
+
|
|
39
|
+
if (!scroller || !hasMessages || !messagesQuery.isSuccess) return
|
|
40
|
+
|
|
41
|
+
const handleShowBtn = () => {
|
|
42
|
+
const scrollPosition =
|
|
43
|
+
Number(scroller.scrollHeight) - Number(scroller.scrollTop) - Number(scroller.clientHeight)
|
|
44
|
+
|
|
45
|
+
setShowScrollButton(scrollPosition > threshold)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
scroller.addEventListener('scroll', handleShowBtn)
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
scroller.removeEventListener('scroll', handleShowBtn)
|
|
52
|
+
}
|
|
53
|
+
}, [hasMessages, messagesQuery.isSuccess, scrollerRef])
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const scroller = scrollerRef.current
|
|
57
|
+
|
|
58
|
+
if (scroller && !messagesQuery.isFetching) {
|
|
59
|
+
scroller.scrollTop = scroller.scrollHeight - heightBeforeRender
|
|
60
|
+
}
|
|
61
|
+
}, [heightBeforeRender, messagesQuery.isFetching, scrollerRef])
|
|
62
|
+
|
|
63
|
+
return { showScrollButton }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default useManageScroll
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { chance } from '@/src/config/tests'
|
|
2
|
+
import ParsedMessageBuilder from '../../__tests__/parsed-message.builder'
|
|
3
|
+
import type { ParsedMessage } from '../../types'
|
|
4
|
+
|
|
5
|
+
import hasToUpdateCursor from './has-to-update-cursor'
|
|
6
|
+
|
|
7
|
+
describe('hasToUpdateCursor', () => {
|
|
8
|
+
const messages = [new ParsedMessageBuilder()]
|
|
9
|
+
const currentCursor = chance.timestamp()
|
|
10
|
+
const profileId = messages?.at?.(0)?.contact.id ?? ''
|
|
11
|
+
|
|
12
|
+
it('should return false when given a empty profileId', () => {
|
|
13
|
+
expect(hasToUpdateCursor({ messages, currentCursor, profileId: '' })).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should return false when given a empty messages list', () => {
|
|
17
|
+
expect(hasToUpdateCursor({ messages: [], currentCursor, profileId })).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should return false when the given messages list last item contact.id is equals to `hotmartContactId`', () => {
|
|
21
|
+
expect(
|
|
22
|
+
hasToUpdateCursor({
|
|
23
|
+
messages: [new ParsedMessageBuilder().withContact({ id: 'hotmartContactId' })],
|
|
24
|
+
currentCursor,
|
|
25
|
+
profileId
|
|
26
|
+
})
|
|
27
|
+
).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return false when the given messages list last item is mine', () => {
|
|
31
|
+
expect(
|
|
32
|
+
hasToUpdateCursor({
|
|
33
|
+
messages: [
|
|
34
|
+
new ParsedMessageBuilder().withContact({ id: profileId }).withMetadata({
|
|
35
|
+
...((messages.at(0)?.metadata ?? {}) as ParsedMessage['metadata']),
|
|
36
|
+
author: 'user'
|
|
37
|
+
})
|
|
38
|
+
],
|
|
39
|
+
currentCursor,
|
|
40
|
+
profileId
|
|
41
|
+
})
|
|
42
|
+
).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should return false when current cursor timestamp is greater than the last message item timestamp', () => {
|
|
46
|
+
expect(
|
|
47
|
+
hasToUpdateCursor({
|
|
48
|
+
messages: [new ParsedMessageBuilder().withTimestamp(currentCursor - 1)],
|
|
49
|
+
currentCursor,
|
|
50
|
+
profileId
|
|
51
|
+
})
|
|
52
|
+
).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should return true when it has to update the cursor', () => {
|
|
56
|
+
expect(hasToUpdateCursor({ messages, currentCursor, profileId })).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ParsedMessage } from '../../types'
|
|
2
|
+
|
|
3
|
+
export type HasToUpdateCursorProps = {
|
|
4
|
+
messages: Array<ParsedMessage>
|
|
5
|
+
currentCursor: number
|
|
6
|
+
profileId: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hasToUpdateCursor({
|
|
10
|
+
currentCursor,
|
|
11
|
+
messages,
|
|
12
|
+
profileId
|
|
13
|
+
}: HasToUpdateCursorProps): boolean {
|
|
14
|
+
if (!profileId || !messages?.length) return false
|
|
15
|
+
|
|
16
|
+
const lastMessage = messages
|
|
17
|
+
.slice()
|
|
18
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
19
|
+
?.at(0)
|
|
20
|
+
|
|
21
|
+
if (!lastMessage || lastMessage.contact.id === 'hotmartContactId') return false
|
|
22
|
+
|
|
23
|
+
const isMine = lastMessage.metadata.author === 'user' && lastMessage.contact.id === profileId
|
|
24
|
+
|
|
25
|
+
if (isMine || currentCursor > lastMessage.timestamp) return false
|
|
26
|
+
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default hasToUpdateCursor
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ICursorUpdateBuilder from '../../cursor/__tests__/icursor-update.builder'
|
|
2
|
+
import type { IMessageWithSenderData } from '../../messages'
|
|
3
|
+
import IMessageWithSenderDataMock from '../../messages/__tests__/imessage-with-sender-data.mock'
|
|
4
|
+
|
|
5
|
+
export const SparkieMessageServiceMock = {
|
|
6
|
+
getAll: vi
|
|
7
|
+
.fn()
|
|
8
|
+
.mockReturnValue(new IMessageWithSenderDataMock().getMany(10) as IMessageWithSenderData[]),
|
|
9
|
+
post: vi.fn(),
|
|
10
|
+
postDirect: vi.fn(),
|
|
11
|
+
remove: vi.fn()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SparkieCursorServiceMock = {
|
|
15
|
+
update: vi.fn(() => new ICursorUpdateBuilder())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SparkieDefaultMethodsMock = {
|
|
19
|
+
destroy: vi.fn(),
|
|
20
|
+
init: vi.fn(),
|
|
21
|
+
off: vi.fn(),
|
|
22
|
+
on: vi.fn(),
|
|
23
|
+
listener: { trackTyping: vi.fn() },
|
|
24
|
+
setAPIToken: vi.fn()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SparkieMock = vi.fn(() => ({
|
|
28
|
+
...SparkieDefaultMethodsMock,
|
|
29
|
+
messageService: SparkieMessageServiceMock,
|
|
30
|
+
cursorService: SparkieCursorServiceMock
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
export default SparkieMock
|
|
@@ -25,6 +25,12 @@ class WidgetSettingPropsBuilder implements WidgetSettingProps {
|
|
|
25
25
|
this.conversationId = chance.guid()
|
|
26
26
|
this.productId = 4234
|
|
27
27
|
this.productName = 'Berim CDs'
|
|
28
|
+
this.conversationId = chance.guid()
|
|
29
|
+
this.contactId = chance.guid()
|
|
30
|
+
this.clubName = 'Test Standard Club'
|
|
31
|
+
this.membershipId = 'test-standard-club'
|
|
32
|
+
this.tutorName = 'Professor Test'
|
|
33
|
+
this.userId = chance.guid()
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
withHotmartToken(hotmartToken: typeof this.hotmartToken) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@/src/config/tests'
|
|
2
|
+
import type { IMessageWithSenderData } from '@/src/modules/messages'
|
|
3
|
+
import IMessageWithSenderDataMock from '@/src/modules/messages/__tests__/imessage-with-sender-data.mock'
|
|
4
|
+
import WidgetSettingPropsBuilder from '../../__tests__/widget-settings-props.builder'
|
|
5
|
+
import * as Store from '../../store'
|
|
6
|
+
|
|
7
|
+
import ChatPage from './chat-page'
|
|
8
|
+
|
|
9
|
+
describe('ChatPage', () => {
|
|
10
|
+
const defaultSettings = new WidgetSettingPropsBuilder()
|
|
11
|
+
const getMessagesMock = new IMessageWithSenderDataMock().getMany(10) as IMessageWithSenderData[]
|
|
12
|
+
|
|
13
|
+
const renderComponent = () => render(<ChatPage />)
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.spyOn(Store, 'useWidgetSettingsAtomValue').mockReturnValue(defaultSettings)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should render each fetched message item from API', async () => {
|
|
20
|
+
renderComponent()
|
|
21
|
+
|
|
22
|
+
await waitFor(() =>
|
|
23
|
+
expect(screen.getAllByTestId('messages-item')).toHaveLength(getMessagesMock.length)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
expect(screen.getByPlaceholderText(/send_message.field.placeholder/)).toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -7,9 +7,7 @@ function ChatPage() {
|
|
|
7
7
|
|
|
8
8
|
return (
|
|
9
9
|
<>
|
|
10
|
-
<
|
|
11
|
-
<MessagesList />
|
|
12
|
-
</div>
|
|
10
|
+
<MessagesList />
|
|
13
11
|
<div className='border-t border-t-neutral-700 px-5 py-4'>
|
|
14
12
|
<ChatInput name='new-chat-msg-input' ref={chatInputRef} />
|
|
15
13
|
</div>
|
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { getInitSparkieQuery } from '../../hooks'
|
|
1
|
+
import { Spinner } from '@/src/lib/components'
|
|
2
|
+
import { useInitSparkie } from '../../hooks'
|
|
5
3
|
import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
|
|
6
4
|
import { WIDGET_TABS } from '../constants'
|
|
7
5
|
|
|
8
6
|
function WidgetContainer() {
|
|
9
7
|
const [settings] = useWidgetSettingsAtom()
|
|
10
|
-
const
|
|
11
|
-
const queryClient = useQueryClient()
|
|
8
|
+
const sparkieQuery = useInitSparkie(settings)
|
|
12
9
|
const [widgetTabs] = useWidgetTabsAtom()
|
|
13
10
|
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (!initSparkieQuery) return
|
|
16
|
-
void (async () => {
|
|
17
|
-
await queryClient.prefetchQuery(initSparkieQuery)
|
|
18
|
-
})()
|
|
19
|
-
}, [initSparkieQuery, queryClient])
|
|
20
|
-
|
|
21
11
|
if (!settings?.tutorName) return null
|
|
22
12
|
|
|
13
|
+
// TODO: change it for the general API error design from FIGMA as soon as it is available
|
|
14
|
+
if (sparkieQuery.isError)
|
|
15
|
+
return (
|
|
16
|
+
<div className='text-neutral-50'>
|
|
17
|
+
<span>Error initializing sparkie</span>
|
|
18
|
+
<button onClick={() => void sparkieQuery.refetch()}>Try again</button>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (sparkieQuery.isLoading)
|
|
23
|
+
return (
|
|
24
|
+
<div className='flex flex-col items-center justify-center p-8'>
|
|
25
|
+
<Spinner className='inline-flex h-6 w-6 animate-spin text-neutral-200' />
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
|
|
23
29
|
return (
|
|
24
|
-
<div className='flex min-h-svh flex-col items-center justify-center
|
|
30
|
+
<div className='flex min-h-svh flex-col items-center justify-center'>
|
|
25
31
|
<div className='grid h-svh w-full grid-rows-[1fr_max-content]'>
|
|
26
32
|
{WIDGET_TABS[widgetTabs.currentTab]}
|
|
27
33
|
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type DetailedHTMLProps, forwardRef } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
|
|
4
|
+
import { Icon } from '@/src/lib/components'
|
|
5
|
+
|
|
6
|
+
export interface IScrollToBottomButtonProps
|
|
7
|
+
extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
|
|
8
|
+
show?: boolean
|
|
9
|
+
top?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ScrollToBottomButton = forwardRef<HTMLButtonElement, IScrollToBottomButtonProps>(
|
|
13
|
+
({ show = false, top = 0, onClick, className, ...props }, ref) => (
|
|
14
|
+
<button
|
|
15
|
+
{...props}
|
|
16
|
+
style={{ top }}
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={clsx(
|
|
19
|
+
'fixed inset-x-1/2 flex size-7 cursor-pointer flex-col items-center justify-center rounded-full bg-neutral-600 text-sm text-neutral-50 outline-none transition-colors duration-300 ease-in hover:scale-110 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2',
|
|
20
|
+
{ 'pointer-events-none opacity-0': !show },
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
onClick={onClick}
|
|
24
|
+
aria-label='Scroller Button'>
|
|
25
|
+
<Icon name='arrow-down' className='size-4' aria-label='Scroller Button Icon' />
|
|
26
|
+
</button>
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ScrollToBottomButton.displayName = 'ScrollToBottomButton'
|
|
31
|
+
|
|
32
|
+
export default ScrollToBottomButton
|
|
@@ -4,15 +4,17 @@ import { SparkieService } from '@/src/modules/sparkie'
|
|
|
4
4
|
import type { WidgetSettingProps } from '@/src/types'
|
|
5
5
|
|
|
6
6
|
export const getInitSparkieQuery = (settings: WidgetSettingProps) => ({
|
|
7
|
-
queryKey: ['SparkieService:initializeSparkie', settings
|
|
7
|
+
queryKey: ['SparkieService:initializeSparkie', settings?.hotmartToken ?? ''],
|
|
8
8
|
queryFn: () =>
|
|
9
9
|
SparkieService.initSparkie({
|
|
10
|
-
token: settings
|
|
10
|
+
token: settings?.hotmartToken,
|
|
11
11
|
skipPresenceSetup: true
|
|
12
|
-
})
|
|
13
|
-
enabled: Boolean(settings?.hotmartToken?.trim())
|
|
12
|
+
})
|
|
14
13
|
})
|
|
15
14
|
|
|
16
|
-
export function useInitSparkie(settings: WidgetSettingProps) {
|
|
17
|
-
return useQuery(
|
|
15
|
+
export function useInitSparkie(settings: WidgetSettingProps | null) {
|
|
16
|
+
return useQuery({
|
|
17
|
+
...getInitSparkieQuery(settings as WidgetSettingProps),
|
|
18
|
+
enabled: Boolean(settings?.hotmartToken?.trim())
|
|
19
|
+
})
|
|
18
20
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { atom, useAtom, useAtomValue } from 'jotai'
|
|
2
|
+
|
|
3
|
+
const intrinsicHeightAtom = atom('100svh')
|
|
4
|
+
const setIntrinsicHeightAtom = atom(
|
|
5
|
+
(get) => get(intrinsicHeightAtom),
|
|
6
|
+
(_, set, value: string) => {
|
|
7
|
+
set(intrinsicHeightAtom, value)
|
|
8
|
+
}
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export const useIntrinsicHeightAtom = () => useAtom(setIntrinsicHeightAtom)
|
|
12
|
+
|
|
13
|
+
export const useIntrinsicHeightAtomValue = () => useAtomValue(intrinsicHeightAtom)
|