forked from dev-xo/remix-saas
-
Notifications
You must be signed in to change notification settings - Fork 1
/
misc.ts
90 lines (81 loc) · 2.42 KB
/
misc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import type { SerializeFrom } from '@remix-run/node'
import type { ClassValue } from 'clsx'
import type { loader as rootLoader } from '#app/root'
import { useFormAction, useNavigation, useRouteLoaderData } from '@remix-run/react'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
/**
* Tailwind CSS classnames with support for conditional classes.
* Widely used for Radix components.
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Use root-loader data.
*/
function isUser(user: any): user is SerializeFrom<typeof rootLoader>['user'] {
return user && typeof user === 'object' && typeof user.id === 'string'
}
export function useOptionalUser() {
const data = useRouteLoaderData<typeof rootLoader>('root')
if (!data || !isUser(data.user)) return undefined
return data.user
}
export function useUser() {
const optionalUser = useOptionalUser()
if (!optionalUser) throw new Error('No user found in root loader.')
return optionalUser
}
/**
* Permissions.
* Implementation based on github.com/epicweb-dev/epic-stack
*/
export type RoleName = 'user' | 'admin'
export function userHasRole(
user: Pick<ReturnType<typeof useUser>, 'roles'> | null,
role: RoleName,
) {
if (!user) return false
return user.roles.some((r) => r.name === role)
}
/**
* Get the user's image src.
*/
export function getUserImgSrc(imageId?: string | null) {
return imageId ? `/resources/user-images/${imageId}` : ''
}
/**
* Use the current route's form action.
* Checks if the current route's form is being submitted.
*
* @default formMethod is POST.
* @default state is non-idle.
*/
export function useIsPending({
formAction,
formMethod = 'POST',
state = 'non-idle',
}: {
formAction?: string
formMethod?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE'
state?: 'submitting' | 'loading' | 'non-idle'
} = {}) {
const contextualFormAction = useFormAction()
const navigation = useNavigation()
const isPendingState =
state === 'non-idle' ? navigation.state !== 'idle' : navigation.state === state
return (
isPendingState &&
navigation.formAction === (formAction ?? contextualFormAction) &&
navigation.formMethod === formMethod
)
}
/**
* Returns a function that calls all of its arguments.
*/
export function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach((fn) => fn?.(...args))
}