@brdgapp/ui 0.1.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/src/types.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export type NetworkContact = {
4
+ id: string
5
+ name: string
6
+ email: string | null
7
+ shareLink: string | null
8
+ profilePicUrl: string | null
9
+ position: string | null
10
+ company: string | null
11
+ username: string | null
12
+ isMember: boolean
13
+ }
14
+
15
+ export type NetworkPersonAction = {
16
+ id: string
17
+ label: string
18
+ description?: string
19
+ icon?: ReactNode
20
+ onSelect: (contact: NetworkContact) => void
21
+ }
22
+
23
+ export type HelperPlacement = "bottom-end" | "bottom-start" | "top-end" | "top-start"
24
+
25
+ export type BridgeClientConfig = {
26
+ apiBaseUrl: string
27
+ getAccessToken: () => Promise<string | null> | string | null
28
+ introInboxUrl: string
29
+ profileUrlForUsername: (username: string) => string
30
+ }
@@ -0,0 +1,122 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import { fetchNetworkPage, PAGE_SIZE } from "./api"
5
+ import { cleanDomain } from "./domain"
6
+ import { useBridgeUI } from "./provider"
7
+ import type { NetworkContact } from "./types"
8
+
9
+ type Options = {
10
+ domain: string
11
+ enabled: boolean
12
+ search: string
13
+ previewContacts: NetworkContact[]
14
+ totalCount: number
15
+ }
16
+
17
+ export function useNetworkPeopleList({
18
+ domain,
19
+ enabled,
20
+ search,
21
+ previewContacts,
22
+ totalCount,
23
+ }: Options) {
24
+ const config = useBridgeUI()
25
+ const cleaned = cleanDomain(domain)
26
+ const searchTerm = search.trim()
27
+ const previewKey = previewContacts.map((contact) => contact.id).join("\n")
28
+ const [pages, setPages] = useState<NetworkContact[][]>([])
29
+ const [hasNextPage, setHasNextPage] = useState(false)
30
+ const [isLoading, setIsLoading] = useState(false)
31
+ const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)
32
+ const [isError, setIsError] = useState(false)
33
+ const requestRef = useRef(0)
34
+ const pagesRef = useRef(pages)
35
+ pagesRef.current = pages
36
+ const previewRef = useRef(previewContacts)
37
+ previewRef.current = previewContacts
38
+
39
+ const load = useCallback(
40
+ async (offset: number, append: boolean, requestId: number) => {
41
+ if (!config || !cleaned) return
42
+ if (append) setIsFetchingNextPage(true)
43
+ else setIsLoading(true)
44
+ try {
45
+ const contacts = await fetchNetworkPage(config, cleaned, { offset, search: searchTerm })
46
+ if (requestRef.current !== requestId) return
47
+ const base = append ? pagesRef.current.reduce((total, page) => total + page.length, 0) : 0
48
+ const nextPages = append ? [...pagesRef.current, contacts] : [contacts]
49
+ pagesRef.current = nextPages
50
+ setPages(nextPages)
51
+ const loaded = base + contacts.length
52
+ const reachedEnd = !searchTerm && totalCount > 0 && loaded >= totalCount
53
+ setHasNextPage(!reachedEnd && contacts.length >= PAGE_SIZE)
54
+ setIsError(false)
55
+ } catch {
56
+ if (requestRef.current !== requestId) return
57
+ if (!append) {
58
+ pagesRef.current = []
59
+ setPages([])
60
+ setIsError(true)
61
+ }
62
+ setHasNextPage(false)
63
+ } finally {
64
+ if (requestRef.current === requestId) {
65
+ setIsLoading(false)
66
+ setIsFetchingNextPage(false)
67
+ }
68
+ }
69
+ },
70
+ [config, cleaned, searchTerm, totalCount],
71
+ )
72
+
73
+ useEffect(() => {
74
+ if (!enabled || !config || !cleaned) {
75
+ pagesRef.current = []
76
+ setPages([])
77
+ setHasNextPage(false)
78
+ setIsLoading(false)
79
+ setIsFetchingNextPage(false)
80
+ setIsError(false)
81
+ return
82
+ }
83
+
84
+ const requestId = ++requestRef.current
85
+ const seeded = !searchTerm ? previewRef.current : []
86
+ if (seeded.length > 0) {
87
+ pagesRef.current = [seeded]
88
+ setPages([seeded])
89
+ setIsError(false)
90
+ setIsLoading(false)
91
+ if (totalCount > 0 && seeded.length >= totalCount) {
92
+ setHasNextPage(false)
93
+ } else {
94
+ setHasNextPage(true)
95
+ void load(seeded.length, true, requestId)
96
+ }
97
+ } else {
98
+ pagesRef.current = []
99
+ setPages([])
100
+ void load(0, false, requestId)
101
+ }
102
+
103
+ return () => {
104
+ requestRef.current += 1
105
+ }
106
+ }, [enabled, config, cleaned, searchTerm, previewKey, totalCount, load])
107
+
108
+ const fetchNextPage = useCallback(() => {
109
+ if (!hasNextPage || isFetchingNextPage || isLoading) return
110
+ const loaded = pagesRef.current.reduce((total, page) => total + page.length, 0)
111
+ void load(loaded, true, requestRef.current)
112
+ }, [hasNextPage, isFetchingNextPage, isLoading, load])
113
+
114
+ return {
115
+ people: pages.flat(),
116
+ isLoading,
117
+ isFetchingNextPage,
118
+ hasNextPage,
119
+ fetchNextPage,
120
+ isError,
121
+ }
122
+ }
@@ -0,0 +1,43 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { fetchNetworkPreview } from "./api"
5
+ import { cleanDomain } from "./domain"
6
+ import { useBridgeUI } from "./provider"
7
+ import type { NetworkContact } from "./types"
8
+
9
+ export type NetworkPreviewState = {
10
+ status: "idle" | "loading" | "ready" | "error"
11
+ count: number
12
+ contacts: NetworkContact[]
13
+ }
14
+
15
+ const IDLE: NetworkPreviewState = { status: "idle", count: 0, contacts: [] }
16
+
17
+ export function useNetworkPreview(domain: string, enabled: boolean): NetworkPreviewState {
18
+ const config = useBridgeUI()
19
+ const cleaned = cleanDomain(domain)
20
+ const [state, setState] = useState<NetworkPreviewState & { domain: string }>({
21
+ ...IDLE,
22
+ domain: cleaned,
23
+ })
24
+
25
+ useEffect(() => {
26
+ if (!enabled || !config || !cleaned) return
27
+ let cancelled = false
28
+ setState({ status: "loading", count: 0, contacts: [], domain: cleaned })
29
+ fetchNetworkPreview(config, cleaned)
30
+ .then((data) => {
31
+ if (!cancelled) setState({ status: "ready", domain: cleaned, ...data })
32
+ })
33
+ .catch(() => {
34
+ if (!cancelled) setState({ status: "error", count: 0, contacts: [], domain: cleaned })
35
+ })
36
+ return () => {
37
+ cancelled = true
38
+ }
39
+ }, [enabled, config, cleaned])
40
+
41
+ if (state.domain !== cleaned) return IDLE
42
+ return state
43
+ }