@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/README.md +223 -0
- package/package.json +28 -0
- package/src/api.ts +173 -0
- package/src/contact-avatar.tsx +63 -0
- package/src/domain.ts +14 -0
- package/src/icons.tsx +130 -0
- package/src/index.ts +12 -0
- package/src/network-people-helper.tsx +397 -0
- package/src/network-people-nudge.tsx +98 -0
- package/src/open-helper.ts +22 -0
- package/src/person-menu.tsx +210 -0
- package/src/provider.tsx +53 -0
- package/src/styles.css +848 -0
- package/src/types.ts +30 -0
- package/src/use-network-people-list.ts +122 -0
- package/src/use-network-preview.ts +43 -0
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
|
+
}
|