@connectycube/react-ui-kit 0.0.17 → 0.0.18
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/configs/dependencies.json +21 -0
- package/configs/imports.json +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/components/avatar.d.ts +2 -2
- package/dist/types/components/avatar.d.ts.map +1 -1
- package/dist/types/components/badge.d.ts +2 -2
- package/dist/types/components/badge.d.ts.map +1 -1
- package/dist/types/components/dialog-item.d.ts +46 -0
- package/dist/types/components/dialog-item.d.ts.map +1 -0
- package/dist/types/components/file-picker.d.ts +22 -0
- package/dist/types/components/file-picker.d.ts.map +1 -0
- package/dist/types/components/formatted-date.d.ts +8 -0
- package/dist/types/components/formatted-date.d.ts.map +1 -0
- package/dist/types/components/input.d.ts.map +1 -1
- package/dist/types/components/label.d.ts +5 -0
- package/dist/types/components/label.d.ts.map +1 -0
- package/dist/types/components/link-preview.d.ts +21 -0
- package/dist/types/components/link-preview.d.ts.map +1 -0
- package/dist/types/components/linkify-text.d.ts +9 -0
- package/dist/types/components/linkify-text.d.ts.map +1 -0
- package/dist/types/components/spinner.d.ts +3 -1
- package/dist/types/components/spinner.d.ts.map +1 -1
- package/dist/types/components/status-sent.d.ts +7 -0
- package/dist/types/components/status-sent.d.ts.map +1 -0
- package/dist/types/components/stream-view.d.ts.map +1 -1
- package/dist/types/components/utils.d.ts +0 -2
- package/dist/types/components/utils.d.ts.map +1 -1
- package/gen/components/avatar.jsx +13 -3
- package/gen/components/badge.jsx +3 -3
- package/gen/components/button.jsx +2 -2
- package/gen/components/dialog-item.jsx +149 -0
- package/gen/components/file-picker.jsx +200 -0
- package/gen/components/formatted-date.jsx +57 -0
- package/gen/components/label.jsx +22 -0
- package/gen/components/link-preview.jsx +131 -0
- package/gen/components/linkify-text.jsx +31 -0
- package/gen/components/spinner.jsx +29 -5
- package/gen/components/status-sent.jsx +21 -0
- package/gen/components/stream-view.jsx +5 -1
- package/gen/components/utils.js +0 -11
- package/package.json +4 -1
- package/src/components/avatar.tsx +15 -5
- package/src/components/badge.tsx +6 -6
- package/src/components/button.tsx +2 -2
- package/src/components/connectycube-ui/avatar.jsx +54 -0
- package/src/components/connectycube-ui/avatar.tsx +77 -0
- package/src/components/connectycube-ui/badge.jsx +45 -0
- package/src/components/connectycube-ui/badge.tsx +42 -0
- package/src/components/connectycube-ui/dialog-item.jsx +149 -0
- package/src/components/connectycube-ui/dialog-item.tsx +188 -0
- package/src/components/connectycube-ui/file-picker.jsx +200 -0
- package/src/components/connectycube-ui/file-picker.tsx +231 -0
- package/src/components/connectycube-ui/formatted-date.jsx +57 -0
- package/src/components/connectycube-ui/formatted-date.tsx +57 -0
- package/src/components/connectycube-ui/label.jsx +22 -0
- package/src/components/connectycube-ui/label.tsx +23 -0
- package/src/components/connectycube-ui/linkify-text.tsx +40 -0
- package/src/components/connectycube-ui/presence.jsx +81 -0
- package/src/components/connectycube-ui/presence.tsx +96 -0
- package/src/components/connectycube-ui/status-sent.jsx +21 -0
- package/src/components/connectycube-ui/status-sent.tsx +25 -0
- package/src/components/connectycube-ui/utils.js +10 -0
- package/src/components/connectycube-ui/utils.ts +10 -0
- package/src/components/dialog-item.tsx +188 -0
- package/src/components/file-picker.tsx +231 -0
- package/src/components/formatted-date.tsx +57 -0
- package/src/components/input.tsx +1 -1
- package/src/components/label.tsx +23 -0
- package/src/components/link-preview.tsx +149 -0
- package/src/components/linkify-text.tsx +41 -0
- package/src/components/spinner.tsx +31 -5
- package/src/components/status-sent.tsx +25 -0
- package/src/components/stream-view.tsx +5 -1
- package/src/components/utils.ts +0 -11
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { forwardRef, memo } from 'react';
|
|
2
|
+
import { differenceInCalendarDays, format, formatDistanceToNow, isToday } from 'date-fns';
|
|
3
|
+
import { el, enUS, uk, type Locale } from 'date-fns/locale';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
interface FormattedDateProps extends React.ComponentProps<'span'> {
|
|
7
|
+
date?: Date | string | number | null | undefined;
|
|
8
|
+
language?: string;
|
|
9
|
+
distanceToNow?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const locales: Record<string, Locale> = {
|
|
13
|
+
en: enUS,
|
|
14
|
+
el: el,
|
|
15
|
+
ua: uk,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function formatDate(date: string | number | Date, language: string = 'en', distanceToNow = false): string {
|
|
19
|
+
const locale = locales[language] ?? enUS;
|
|
20
|
+
|
|
21
|
+
if (distanceToNow) {
|
|
22
|
+
return formatDistanceToNow(date, { locale, addSuffix: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isWithinLast7Days = (date: Date | string | number) => {
|
|
26
|
+
const diff = differenceInCalendarDays(new Date(), date);
|
|
27
|
+
|
|
28
|
+
return diff >= 0 && diff <= 6;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return isToday(date)
|
|
32
|
+
? format(date, 'p', { locale })
|
|
33
|
+
: isWithinLast7Days(date)
|
|
34
|
+
? format(date, 'eeee', { locale })
|
|
35
|
+
: format(date, 'P', { locale });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function FormattedDateBase(
|
|
39
|
+
{ date, language, distanceToNow, ...props }: FormattedDateProps,
|
|
40
|
+
ref: React.ForwardedRef<HTMLSpanElement>
|
|
41
|
+
) {
|
|
42
|
+
if (!date) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<span ref={ref} {...props} className={cn('text-xs text-muted-foreground', props?.className)}>
|
|
48
|
+
{formatDate(date, language, distanceToNow)}
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const FormattedDate = memo(forwardRef<HTMLSpanElement, FormattedDateProps>(FormattedDateBase));
|
|
54
|
+
|
|
55
|
+
FormattedDate.displayName = 'FormattedDate';
|
|
56
|
+
|
|
57
|
+
export { FormattedDate, type FormattedDateProps };
|
package/src/components/input.tsx
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { Root as LabelRoot, type LabelProps } from '@radix-ui/react-label';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
function LabelBase({ ...props }: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<LabelRoot
|
|
9
|
+
ref={ref}
|
|
10
|
+
{...props}
|
|
11
|
+
className={cn(
|
|
12
|
+
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
13
|
+
props?.className
|
|
14
|
+
)}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Label = forwardRef<HTMLLabelElement, LabelProps>(LabelBase);
|
|
20
|
+
|
|
21
|
+
Label.displayName = 'Label';
|
|
22
|
+
|
|
23
|
+
export { Label, type LabelProps };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef, memo, useCallback, useState } from 'react';
|
|
3
|
+
import { Globe, type LucideProps } from 'lucide-react';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
const decodeHtmlEntities = (text: string) => {
|
|
7
|
+
if (text.length === 0) {
|
|
8
|
+
return text;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const element = document.createElement('div');
|
|
12
|
+
|
|
13
|
+
element.innerHTML = text;
|
|
14
|
+
|
|
15
|
+
return element.textContent.trim();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface LinkPreviewProps extends React.ComponentProps<'a'> {
|
|
19
|
+
thin?: boolean;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
icon?: React.ComponentProps<'img'>['src'];
|
|
23
|
+
image?: React.ComponentProps<'img'>['src'];
|
|
24
|
+
onReady?: () => void;
|
|
25
|
+
iconFallbackElement?: React.ReactElement;
|
|
26
|
+
titleContainerProps?: React.ComponentProps<'div'>;
|
|
27
|
+
iconProps?: React.ComponentProps<'img'>;
|
|
28
|
+
iconFallbackProps?: LucideProps;
|
|
29
|
+
titleProps?: React.ComponentProps<'span'>;
|
|
30
|
+
descriptionProps?: React.ComponentProps<'div'>;
|
|
31
|
+
imageContainerProps?: React.ComponentProps<'div'>;
|
|
32
|
+
imageProps?: React.ComponentProps<'img'>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function LinkPreviewBase(
|
|
36
|
+
{
|
|
37
|
+
thin = false,
|
|
38
|
+
title = '',
|
|
39
|
+
description = '',
|
|
40
|
+
icon,
|
|
41
|
+
iconFallbackElement,
|
|
42
|
+
image,
|
|
43
|
+
onReady = () => {},
|
|
44
|
+
titleContainerProps,
|
|
45
|
+
iconProps,
|
|
46
|
+
iconFallbackProps,
|
|
47
|
+
titleProps,
|
|
48
|
+
descriptionProps,
|
|
49
|
+
imageContainerProps,
|
|
50
|
+
imageProps,
|
|
51
|
+
...props
|
|
52
|
+
}: LinkPreviewProps,
|
|
53
|
+
ref: React.ForwardedRef<HTMLAnchorElement>
|
|
54
|
+
) {
|
|
55
|
+
const [iconSrc, setIconSrc] = useState<React.ComponentProps<'img'>['src']>(icon);
|
|
56
|
+
const [imageSrc, setImageSrc] = useState<React.ComponentProps<'img'>['src']>(image);
|
|
57
|
+
const handleOnLoad = useCallback(
|
|
58
|
+
(event: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
|
59
|
+
imageProps?.onLoad?.(event);
|
|
60
|
+
onReady();
|
|
61
|
+
},
|
|
62
|
+
[onReady, imageProps]
|
|
63
|
+
);
|
|
64
|
+
const handleIconOnError = useCallback(
|
|
65
|
+
(event: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
|
66
|
+
iconProps?.onError?.(event);
|
|
67
|
+
setIconSrc(undefined);
|
|
68
|
+
},
|
|
69
|
+
[iconProps]
|
|
70
|
+
);
|
|
71
|
+
const handleImageOnError = useCallback(
|
|
72
|
+
(event: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
|
73
|
+
imageProps?.onError?.(event);
|
|
74
|
+
setImageSrc(undefined);
|
|
75
|
+
},
|
|
76
|
+
[imageProps]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<a
|
|
81
|
+
ref={ref}
|
|
82
|
+
target="_blank"
|
|
83
|
+
rel="noopener noreferrer"
|
|
84
|
+
{...props}
|
|
85
|
+
className={cn(
|
|
86
|
+
'transition-color duration-300 ease-out',
|
|
87
|
+
'grid items-start gap-2 p-2 mt-1 overflow-hidden rounded border-l-4 border-l-ring bg-white hover:bg-ring/20',
|
|
88
|
+
thin ? 'grid-cols-3' : 'grid-cols-1',
|
|
89
|
+
props?.className
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
{...titleContainerProps}
|
|
94
|
+
className={cn('flex items-start gap-2', thin ? 'col-span-3' : 'col-span-1', titleContainerProps?.className)}
|
|
95
|
+
>
|
|
96
|
+
{iconSrc ? (
|
|
97
|
+
<img
|
|
98
|
+
alt="icon"
|
|
99
|
+
src={iconSrc}
|
|
100
|
+
{...iconProps}
|
|
101
|
+
onError={handleIconOnError}
|
|
102
|
+
className={cn('size-5', iconProps?.className)}
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
iconFallbackElement || <Globe {...iconFallbackProps} className={cn('size-5', iconFallbackProps?.className)} />
|
|
106
|
+
)}
|
|
107
|
+
<span
|
|
108
|
+
{...titleProps}
|
|
109
|
+
className={cn('text-sm font-bold', thin ? 'line-clamp-1' : 'line-clamp-2', titleProps?.className)}
|
|
110
|
+
>
|
|
111
|
+
{decodeHtmlEntities(title)}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
{description && (
|
|
115
|
+
<span
|
|
116
|
+
{...descriptionProps}
|
|
117
|
+
className={cn(
|
|
118
|
+
'text-sm line-clamp-5',
|
|
119
|
+
thin ? (imageSrc ? 'col-span-2' : 'col-span-3') : 'col-span-1',
|
|
120
|
+
descriptionProps?.className
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{decodeHtmlEntities(description)}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
{imageSrc && (
|
|
127
|
+
<div
|
|
128
|
+
{...imageContainerProps}
|
|
129
|
+
className={cn('flex items-center justify-center', imageContainerProps?.className)}
|
|
130
|
+
>
|
|
131
|
+
<img
|
|
132
|
+
alt="banner"
|
|
133
|
+
src={imageSrc}
|
|
134
|
+
{...imageProps}
|
|
135
|
+
onLoad={handleOnLoad}
|
|
136
|
+
onError={handleImageOnError}
|
|
137
|
+
className={cn('rounded max-w-full object-contain', thin ? 'max-h-25' : 'max-h-60', imageProps?.className)}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</a>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const LinkPreview = memo(forwardRef<HTMLAnchorElement, LinkPreviewProps>(LinkPreviewBase));
|
|
146
|
+
|
|
147
|
+
LinkPreview.displayName = 'LinkPreview';
|
|
148
|
+
|
|
149
|
+
export { LinkPreview, type LinkPreviewProps };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { Opts } from 'linkifyjs';
|
|
3
|
+
import { forwardRef, memo, useMemo } from 'react';
|
|
4
|
+
import Linkify from 'linkify-react';
|
|
5
|
+
import { cn } from './utils';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LINKIFY_OPTIONS: Opts = {
|
|
8
|
+
target: '_blank',
|
|
9
|
+
rel: 'noopener noreferrer',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface LinkifyTextProps extends React.ComponentProps<'p'> {
|
|
13
|
+
text: string;
|
|
14
|
+
linkifyProps?: Opts;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function LinkifyTextBase(
|
|
18
|
+
{ text, linkifyProps, ...props }: LinkifyTextProps,
|
|
19
|
+
ref: React.ForwardedRef<HTMLParagraphElement>
|
|
20
|
+
) {
|
|
21
|
+
const options = useMemo(
|
|
22
|
+
() => ({
|
|
23
|
+
...DEFAULT_LINKIFY_OPTIONS,
|
|
24
|
+
...linkifyProps,
|
|
25
|
+
className: cn('text-blue-500 hover:text-blue-600 hover:underline', linkifyProps?.className),
|
|
26
|
+
}),
|
|
27
|
+
[linkifyProps]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<p ref={ref} {...props} className={cn('wrap-break-word text-base', props?.className)}>
|
|
32
|
+
<Linkify options={options}>{text}</Linkify>
|
|
33
|
+
</p>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const LinkifyText = memo(forwardRef<HTMLParagraphElement, LinkifyTextProps>(LinkifyTextBase));
|
|
38
|
+
|
|
39
|
+
LinkifyText.displayName = 'LinkifyText';
|
|
40
|
+
|
|
41
|
+
export { LinkifyText, type LinkifyTextProps };
|
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
import { LoaderCircle, type LucideProps } from 'lucide-react';
|
|
1
|
+
import { Loader, LoaderCircle, type LucideProps } from 'lucide-react';
|
|
2
2
|
import { cn } from './utils';
|
|
3
3
|
|
|
4
4
|
interface SpinnerProps extends LucideProps {
|
|
5
5
|
loading?: boolean;
|
|
6
|
+
type?: 'default' | 'circle';
|
|
7
|
+
layout?: 'absolute' | 'centered' | 'overlay' | 'flow';
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
function Spinner({ loading =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
)
|
|
10
|
+
function Spinner({ loading = false, layout = 'flow', type = 'default', ...props }: SpinnerProps) {
|
|
11
|
+
const LoaderIcon = type === 'circle' ? LoaderCircle : Loader;
|
|
12
|
+
|
|
13
|
+
if (!loading) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const spinnerElement = (
|
|
18
|
+
<LoaderIcon
|
|
19
|
+
strokeWidth={2.5}
|
|
20
|
+
{...props}
|
|
21
|
+
className={cn('animate-spin text-ring', layout === 'flow' && 'mx-auto', props?.className)}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
switch (layout) {
|
|
26
|
+
case 'absolute':
|
|
27
|
+
return (
|
|
28
|
+
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">{spinnerElement}</div>
|
|
29
|
+
);
|
|
30
|
+
case 'centered':
|
|
31
|
+
return <div className="flex items-center justify-center size-full">{spinnerElement}</div>;
|
|
32
|
+
case 'overlay':
|
|
33
|
+
return <div className="flex items-center justify-center size-full absolute bg-muted/50">{spinnerElement}</div>;
|
|
34
|
+
case 'flow':
|
|
35
|
+
default:
|
|
36
|
+
return spinnerElement;
|
|
37
|
+
}
|
|
12
38
|
}
|
|
13
39
|
|
|
14
40
|
Spinner.displayName = 'Spinner';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Ban, Check, CheckCheck, Clock, type LucideProps } from 'lucide-react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
|
|
4
|
+
interface StatusSentProps extends LucideProps {
|
|
5
|
+
status?: 'wait' | 'sent' | 'read' | 'lost' | null | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const StatusSent: React.FC<StatusSentProps> = ({ status, ...props }) => {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case 'wait':
|
|
11
|
+
return <Clock {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
12
|
+
case 'sent':
|
|
13
|
+
return <Check {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
14
|
+
case 'read':
|
|
15
|
+
return <CheckCheck {...props} className={cn('text-gray-500 size-4', props?.className)} />;
|
|
16
|
+
case 'lost':
|
|
17
|
+
return <Ban {...props} className={cn('text-red-500 size-4', props?.className)} />;
|
|
18
|
+
default:
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
StatusSent.displayName = 'StatusSent';
|
|
24
|
+
|
|
25
|
+
export { StatusSent, type StatusSentProps };
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Maximize, Minimize, PictureInPicture2, type LucideProps } from 'lucide-react';
|
|
4
|
-
import { cn
|
|
4
|
+
import { cn } from './utils';
|
|
5
5
|
|
|
6
6
|
interface StreamViewProps extends React.ComponentProps<'video'> {
|
|
7
7
|
stream?: MediaStream | null;
|
|
8
8
|
mirror?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function getRandomString(length = 8): string {
|
|
12
|
+
return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
function StreamViewBase(
|
|
12
16
|
{ id, stream, mirror, className, muted, ...props }: StreamViewProps,
|
|
13
17
|
ref: React.ForwardedRef<HTMLVideoElement>
|
package/src/components/utils.ts
CHANGED
|
@@ -5,17 +5,6 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
5
5
|
return twMerge(clsx(inputs));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function getRandomString(length = 8): string {
|
|
9
|
-
return (Date.now() / Math.random()).toString(36).replace('.', '').slice(0, length);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getInitialsFromName(name?: string): string {
|
|
13
|
-
const words = name?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
14
|
-
const result = words.length > 1 ? `${words[0]?.[0]}${words[1]?.[0]}` : (words[0]?.slice(0, 2) ?? 'NA');
|
|
15
|
-
|
|
16
|
-
return result.toUpperCase();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
8
|
export function capitalize(str?: string): string {
|
|
20
9
|
return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
|
|
21
10
|
}
|