@m5kdev/web-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.
Files changed (127) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +17 -0
  3. package/package.json +169 -0
  4. package/src/animations/card.motion.ts +9 -0
  5. package/src/components/AvatarUpload.tsx +133 -0
  6. package/src/components/Button.tsx +14 -0
  7. package/src/components/Calendar.css +684 -0
  8. package/src/components/Calendar.tsx +32 -0
  9. package/src/components/CardsSelect.tsx +155 -0
  10. package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
  11. package/src/components/ColorPicker.tsx +56 -0
  12. package/src/components/CopyButton.tsx +45 -0
  13. package/src/components/CropDialog.tsx +154 -0
  14. package/src/components/DialogProvider.tsx +105 -0
  15. package/src/components/ErrorFallback.tsx +17 -0
  16. package/src/components/FileDropzone.tsx +120 -0
  17. package/src/components/MultiSelectDropdown.tsx +233 -0
  18. package/src/components/Orb.tsx +288 -0
  19. package/src/components/PageAlert.tsx +121 -0
  20. package/src/components/SelectChips.tsx +40 -0
  21. package/src/components/SidebarItem.tsx +26 -0
  22. package/src/components/Steps.tsx +340 -0
  23. package/src/components/TablerIconPicker.tsx +4260 -0
  24. package/src/components/app-header.tsx +40 -0
  25. package/src/components/blur-card.tsx +132 -0
  26. package/src/components/features-section-demo-1.tsx +127 -0
  27. package/src/components/features-section-demo-2.tsx +102 -0
  28. package/src/components/features-section-demo-3.tsx +272 -0
  29. package/src/components/mode-toggle.tsx +31 -0
  30. package/src/components/nav-main.tsx +69 -0
  31. package/src/components/pricing-cards.tsx +133 -0
  32. package/src/components/shared/ButtonCopy.tsx +50 -0
  33. package/src/components/team-switcher.tsx +83 -0
  34. package/src/components/theme-provider.tsx +74 -0
  35. package/src/components/typewriter.tsx +90 -0
  36. package/src/components/ui/alert-dialog.tsx +133 -0
  37. package/src/components/ui/alert.tsx +60 -0
  38. package/src/components/ui/avatar.tsx +47 -0
  39. package/src/components/ui/badge.tsx +33 -0
  40. package/src/components/ui/bento-grid.tsx +54 -0
  41. package/src/components/ui/bento-grid2.tsx +66 -0
  42. package/src/components/ui/breadcrumb.tsx +101 -0
  43. package/src/components/ui/button.tsx +50 -0
  44. package/src/components/ui/card.tsx +55 -0
  45. package/src/components/ui/checkbox.tsx +26 -0
  46. package/src/components/ui/collapsible.tsx +9 -0
  47. package/src/components/ui/dialog.tsx +119 -0
  48. package/src/components/ui/dropdown-menu.tsx +186 -0
  49. package/src/components/ui/floating-navbar.tsx +78 -0
  50. package/src/components/ui/form.tsx +167 -0
  51. package/src/components/ui/image.tsx +55 -0
  52. package/src/components/ui/input.tsx +22 -0
  53. package/src/components/ui/label.tsx +19 -0
  54. package/src/components/ui/pagination.tsx +105 -0
  55. package/src/components/ui/progress.tsx +23 -0
  56. package/src/components/ui/resizable-navbar.tsx +260 -0
  57. package/src/components/ui/segment-control.tsx +143 -0
  58. package/src/components/ui/select.tsx +153 -0
  59. package/src/components/ui/separator.tsx +24 -0
  60. package/src/components/ui/sheet.tsx +121 -0
  61. package/src/components/ui/sidebar.tsx +736 -0
  62. package/src/components/ui/skeleton.tsx +7 -0
  63. package/src/components/ui/slider.tsx +23 -0
  64. package/src/components/ui/sonner.tsx +27 -0
  65. package/src/components/ui/spinner.tsx +45 -0
  66. package/src/components/ui/switch.tsx +27 -0
  67. package/src/components/ui/table.tsx +90 -0
  68. package/src/components/ui/tabs.tsx +52 -0
  69. package/src/components/ui/textarea.tsx +18 -0
  70. package/src/components/ui/timeline.tsx +95 -0
  71. package/src/components/ui/toast.tsx +126 -0
  72. package/src/components/ui/tooltip.tsx +55 -0
  73. package/src/components/ui/typewriter-effect.tsx +181 -0
  74. package/src/hooks/use-mobile.ts +19 -0
  75. package/src/hooks/useDialog.ts +25 -0
  76. package/src/icons/GoogleIcon.tsx +32 -0
  77. package/src/icons/LinkedInIcon.tsx +30 -0
  78. package/src/icons/MicrosoftIcon.tsx +21 -0
  79. package/src/lib/chatwoot.ts +51 -0
  80. package/src/lib/utils.ts +6 -0
  81. package/src/modules/app/components/AppLoader.tsx +9 -0
  82. package/src/modules/app/components/AppShell.tsx +21 -0
  83. package/src/modules/app/components/AppSidebar.tsx +26 -0
  84. package/src/modules/app/components/AppSidebarContent.tsx +73 -0
  85. package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
  86. package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
  87. package/src/modules/app/components/AppSidebarUser.tsx +128 -0
  88. package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
  89. package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
  90. package/src/modules/auth/components/AuthLayout.tsx +13 -0
  91. package/src/modules/auth/components/AuthProviders.tsx +105 -0
  92. package/src/modules/auth/components/AuthRouter.tsx +29 -0
  93. package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
  94. package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
  95. package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
  96. package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
  97. package/src/modules/auth/components/InviteFriends.tsx +273 -0
  98. package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
  99. package/src/modules/auth/components/LoginForm.tsx +104 -0
  100. package/src/modules/auth/components/LoginRoute.tsx +31 -0
  101. package/src/modules/auth/components/LogoutRoute.tsx +21 -0
  102. package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
  103. package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
  104. package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
  105. package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
  106. package/src/modules/auth/components/ProfileRoute.tsx +104 -0
  107. package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
  108. package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
  109. package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
  110. package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
  111. package/src/modules/auth/components/SignupRoute.tsx +53 -0
  112. package/src/modules/auth/components/UserPreferences.tsx +144 -0
  113. package/src/modules/auth/components/WaitlistCard.tsx +78 -0
  114. package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
  115. package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
  116. package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
  117. package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
  118. package/src/modules/billing/components/BillingRouter.tsx +20 -0
  119. package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
  120. package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
  121. package/src/modules/table/components/NuqsTable.tsx +396 -0
  122. package/src/modules/table/components/TableFiltering.tsx +520 -0
  123. package/src/modules/table/components/TablePagination.tsx +59 -0
  124. package/src/modules/table/components/table.types.ts +11 -0
  125. package/src/modules/table/filterTransformers.ts +323 -0
  126. package/src/types.ts +4 -0
  127. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,155 @@
1
+ import { Check, Edit } from "lucide-react";
2
+ import * as React from "react";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "#components/ui/card";
4
+ import { cn } from "#utils";
5
+
6
+ export interface CardSelectItem {
7
+ id: string;
8
+ title: string;
9
+ description?: string;
10
+ content?: React.ReactNode;
11
+ disabled?: boolean;
12
+ icon?: React.ReactNode;
13
+ onEdit?: (id: string) => void;
14
+ }
15
+
16
+ export interface CardsSelectProps {
17
+ items: CardSelectItem[];
18
+ selectedIds: string[];
19
+ onSelectionChange: (selectedIds: string[]) => void;
20
+ maxSelections?: number;
21
+ className?: string;
22
+ cardClassName?: string;
23
+ showCheckbox?: boolean;
24
+ disabled?: boolean;
25
+ reversed?: boolean;
26
+ }
27
+
28
+ const CardsSelect = React.forwardRef<HTMLDivElement, CardsSelectProps>(
29
+ (
30
+ {
31
+ items,
32
+ selectedIds,
33
+ onSelectionChange,
34
+ maxSelections,
35
+ className,
36
+ cardClassName,
37
+ showCheckbox = true,
38
+ disabled = false,
39
+ reversed = false,
40
+ ...props
41
+ },
42
+ ref
43
+ ) => {
44
+ const handleCardClick = (itemId: string) => {
45
+ if (disabled) return;
46
+
47
+ const item = items.find((item) => item.id === itemId);
48
+ if (item?.disabled) return;
49
+
50
+ const isSelected = selectedIds.includes(itemId);
51
+
52
+ if (isSelected) {
53
+ // Remove from selection
54
+ onSelectionChange(selectedIds.filter((id) => id !== itemId));
55
+ } else {
56
+ // Add to selection
57
+ if (maxSelections && selectedIds.length >= maxSelections) {
58
+ // If max selections reached, replace the first selection
59
+ onSelectionChange([...selectedIds.slice(1), itemId]);
60
+ } else {
61
+ onSelectionChange([...selectedIds, itemId]);
62
+ }
63
+ }
64
+ };
65
+
66
+ const getGridClasses = () => {
67
+ return "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 justify-items-center";
68
+ };
69
+
70
+ return (
71
+ <div ref={ref} className={cn(getGridClasses(), className)} {...props}>
72
+ {items.map((item) => {
73
+ const isSelected = reversed
74
+ ? !selectedIds.includes(item.id)
75
+ : selectedIds.includes(item.id);
76
+ const isDisabled = disabled || item.disabled;
77
+
78
+ return (
79
+ <Card
80
+ key={item.id}
81
+ className={cn(
82
+ "relative cursor-pointer transition-all duration-200 hover:shadow-md w-full",
83
+ isSelected && "ring-2 ring-primary bg-primary/5 border-primary",
84
+ isDisabled && "opacity-50 cursor-not-allowed",
85
+ !isDisabled && "hover:bg-accent/50",
86
+ cardClassName
87
+ )}
88
+ onClick={() => handleCardClick(item.id)}
89
+ >
90
+ {/* Edit button */}
91
+ {item.onEdit && (
92
+ <div className="absolute top-3 left-3 z-10">
93
+ <button
94
+ type="button"
95
+ className={cn(
96
+ "flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-accent",
97
+ "text-muted-foreground hover:text-foreground"
98
+ )}
99
+ onClick={(e) => {
100
+ e.stopPropagation();
101
+ item.onEdit?.(item.id);
102
+ }}
103
+ aria-label="Edit"
104
+ >
105
+ <Edit className="h-3.5 w-3.5" />
106
+ </button>
107
+ </div>
108
+ )}
109
+
110
+ {/* Selection indicator */}
111
+ {showCheckbox && (
112
+ <div className="absolute top-3 right-3 z-10">
113
+ <div
114
+ className={cn(
115
+ "flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors",
116
+ isSelected
117
+ ? "bg-primary border-primary text-primary-foreground"
118
+ : "border-muted-foreground/30 bg-background"
119
+ )}
120
+ >
121
+ {isSelected && <Check className="h-3 w-3" />}
122
+ </div>
123
+ </div>
124
+ )}
125
+
126
+ {/* Card icon */}
127
+ {item.icon && (
128
+ <div className="flex items-center justify-center pt-6 pb-4">
129
+ <div className="text-5xl text-primary">
130
+ <i className={`ti ${item.icon}`} />
131
+ </div>
132
+ </div>
133
+ )}
134
+
135
+ <CardHeader className={cn("pb-3 text-center", item.icon && "pt-2")}>
136
+ <CardTitle className="text-lg leading-tight">{item.title}</CardTitle>
137
+ {item.description && (
138
+ <CardDescription className="text-sm">{item.description}</CardDescription>
139
+ )}
140
+ </CardHeader>
141
+
142
+ {item.content && (
143
+ <CardContent className="pt-0 text-center">{item.content}</CardContent>
144
+ )}
145
+ </Card>
146
+ );
147
+ })}
148
+ </div>
149
+ );
150
+ }
151
+ );
152
+
153
+ CardsSelect.displayName = "CardsSelect";
154
+
155
+ export { CardsSelect };
@@ -0,0 +1,57 @@
1
+ import type { CollapsibleProps } from "@radix-ui/react-collapsible";
2
+ import { ChevronRight } from "lucide-react";
3
+ import type { ReactNode } from "react";
4
+ import { Link } from "react-router";
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "#components/ui/collapsible";
6
+ import {
7
+ SidebarMenuButton,
8
+ SidebarMenuItem,
9
+ SidebarMenuSub,
10
+ useSidebar,
11
+ } from "#components/ui/sidebar";
12
+
13
+ export function CollapsibleSidebarMenuItem({
14
+ children,
15
+ label,
16
+ icon,
17
+ link,
18
+ badge,
19
+ ...props
20
+ }: {
21
+ children: ReactNode;
22
+ label: string;
23
+ icon: ReactNode;
24
+ link: string;
25
+ badge?: ReactNode;
26
+ } & CollapsibleProps) {
27
+ const { open } = useSidebar();
28
+
29
+ if (!open)
30
+ return (
31
+ <SidebarMenuItem>
32
+ <SidebarMenuButton asChild tooltip={label}>
33
+ <Link to={link}>
34
+ {icon}
35
+ <span>{label}</span>
36
+ </Link>
37
+ </SidebarMenuButton>
38
+ </SidebarMenuItem>
39
+ );
40
+
41
+ return (
42
+ <Collapsible asChild className="group/collapsible" {...props}>
43
+ <SidebarMenuItem>
44
+ <CollapsibleTrigger asChild>
45
+ <SidebarMenuButton>
46
+ {icon}
47
+ {badge ? badge : <span>{label}</span>}
48
+ <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
49
+ </SidebarMenuButton>
50
+ </CollapsibleTrigger>
51
+ <CollapsibleContent>
52
+ <SidebarMenuSub>{children}</SidebarMenuSub>
53
+ </CollapsibleContent>
54
+ </SidebarMenuItem>
55
+ </Collapsible>
56
+ );
57
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ Button,
3
+ Input,
4
+ type InputProps,
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from "@heroui/react";
9
+ import { hexToHsva } from "@uiw/color-convert";
10
+ import Colorful from "@uiw/react-color-colorful";
11
+ import { PipetteIcon } from "lucide-react";
12
+ import { useState } from "react";
13
+
14
+ function safeHexToHsva(hex: string) {
15
+ try {
16
+ return hexToHsva(hex);
17
+ } catch (err) {
18
+ return { h: 0, s: 0, v: 0, a: 1 };
19
+ }
20
+ }
21
+
22
+ export function ColorPicker({
23
+ disableAlpha = true,
24
+ value,
25
+
26
+ onValueChange,
27
+ ...props
28
+ }: InputProps & { disableAlpha?: boolean }) {
29
+ const [hsva, setHsva] = useState(safeHexToHsva(value || ""));
30
+ return (
31
+ <Input
32
+ onValueChange={onValueChange}
33
+ value={value}
34
+ endContent={
35
+ <Popover placement="bottom">
36
+ <PopoverTrigger>
37
+ <Button isIconOnly aria-label="Pick color" size="sm" variant="light">
38
+ <PipetteIcon className="h-4 w-4 text-foreground" />
39
+ </Button>
40
+ </PopoverTrigger>
41
+ <PopoverContent aria-label="Color picker">
42
+ <Colorful
43
+ color={hsva}
44
+ disableAlpha={disableAlpha}
45
+ onChange={(color) => {
46
+ setHsva(color.hsva);
47
+ onValueChange?.(color.hex);
48
+ }}
49
+ />
50
+ </PopoverContent>
51
+ </Popover>
52
+ }
53
+ {...props}
54
+ />
55
+ );
56
+ }
@@ -0,0 +1,45 @@
1
+ import { Button, type ButtonProps } from "@heroui/react";
2
+ import { CheckCircle, Copy } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { toast } from "sonner";
6
+
7
+ export function CopyButton({
8
+ text,
9
+ notificationTimeout = 1000,
10
+ isIconOnly,
11
+ onCopy,
12
+ ...props
13
+ }: ButtonProps & { text: string; notificationTimeout?: number; onCopy?: () => void }) {
14
+ const { t } = useTranslation();
15
+ const [isCopied, setIsCopied] = useState(false);
16
+
17
+ const handleCopy = async (text: string) => {
18
+ try {
19
+ await window.navigator.clipboard.writeText(text);
20
+ setIsCopied(true);
21
+ onCopy?.();
22
+ toast.success(t("web-ui:common.copySuccess"));
23
+ setTimeout(() => setIsCopied(false), notificationTimeout);
24
+ } catch (error) {
25
+ toast.error(t("web-ui:common.copyError"));
26
+ console.error(error);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <Button isIconOnly={isIconOnly} onPress={() => handleCopy(text)} {...props}>
32
+ {isCopied ? (
33
+ <>
34
+ <CheckCircle className="h-4 w-4" />
35
+ {!isIconOnly && t("web-ui:common.copied")}
36
+ </>
37
+ ) : (
38
+ <>
39
+ <Copy className="h-4 w-4" />
40
+ {!isIconOnly && t("web-ui:common.copy")}
41
+ </>
42
+ )}
43
+ </Button>
44
+ );
45
+ }
@@ -0,0 +1,154 @@
1
+ import { ZoomIn, ZoomOut } from "lucide-react";
2
+ import { useState } from "react";
3
+ import Cropper, { type Area } from "react-easy-crop";
4
+ import { useTranslation } from "react-i18next";
5
+ import { Button } from "#components/ui/button";
6
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "#components/ui/dialog";
7
+ import { Slider } from "#components/ui/slider";
8
+
9
+ interface CropArea {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ }
15
+
16
+ interface CropDialogProps {
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ imageUrl: string;
20
+ onCropComplete: (croppedBlob: Blob) => void;
21
+ onCancel: () => void;
22
+ isLoading?: boolean;
23
+ }
24
+
25
+ async function getCroppedImg(
26
+ imageSrc: string,
27
+ pixelCrop: CropArea,
28
+ t: (key: string) => string
29
+ ): Promise<Blob> {
30
+ const image = new Image();
31
+ image.src = imageSrc;
32
+
33
+ // Create canvas
34
+ const canvas = document.createElement("canvas");
35
+ const ctx = canvas.getContext("2d");
36
+
37
+ if (!ctx) {
38
+ throw new Error(t("web-ui:image.crop.failedContext"));
39
+ }
40
+
41
+ // Set canvas size to the crop size
42
+ canvas.width = pixelCrop.width;
43
+ canvas.height = pixelCrop.height;
44
+
45
+ // Draw the cropped image
46
+ ctx.drawImage(
47
+ image,
48
+ pixelCrop.x,
49
+ pixelCrop.y,
50
+ pixelCrop.width,
51
+ pixelCrop.height,
52
+ 0,
53
+ 0,
54
+ pixelCrop.width,
55
+ pixelCrop.height
56
+ );
57
+
58
+ // Get the cropped image as blob
59
+ return new Promise((resolve, reject) => {
60
+ canvas.toBlob((blob) => {
61
+ if (!blob) reject(new Error(t("web-ui:image.crop.failedCrop")));
62
+ else resolve(blob);
63
+ }, "image/jpeg");
64
+ });
65
+ }
66
+
67
+ export function CropDialog({
68
+ open,
69
+ onOpenChange,
70
+ imageUrl,
71
+ onCropComplete,
72
+ onCancel,
73
+ isLoading = false,
74
+ }: CropDialogProps) {
75
+ const { t } = useTranslation();
76
+ const [crop, setCrop] = useState({ x: 0, y: 0 });
77
+ const [zoom, setZoom] = useState(1);
78
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState<CropArea | null>(null);
79
+
80
+ const handleCropComplete = (_croppedArea: Area, croppedAreaPixels: CropArea) => {
81
+ setCroppedAreaPixels(croppedAreaPixels);
82
+ };
83
+
84
+ const handleSave = async () => {
85
+ if (!croppedAreaPixels) return;
86
+
87
+ try {
88
+ const croppedImageBlob = await getCroppedImg(imageUrl, croppedAreaPixels, t);
89
+ onCropComplete(croppedImageBlob);
90
+ } catch (error) {
91
+ console.error(t("web-ui:image.crop.error"), error);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <Dialog open={open} onOpenChange={onOpenChange}>
97
+ <DialogContent className="sm:max-w-[600px]">
98
+ <DialogHeader>
99
+ <DialogTitle>{t("web-ui:image.crop.title")}</DialogTitle>
100
+ </DialogHeader>
101
+ <div className="relative h-[400px] w-full">
102
+ <Cropper
103
+ image={imageUrl}
104
+ crop={crop}
105
+ zoom={zoom}
106
+ aspect={1}
107
+ onCropChange={setCrop}
108
+ onZoomChange={setZoom}
109
+ onCropComplete={handleCropComplete}
110
+ objectFit="contain"
111
+ showGrid={true}
112
+ style={{
113
+ containerStyle: {
114
+ position: "relative",
115
+ width: "100%",
116
+ height: "100%",
117
+ backgroundColor: "#262626",
118
+ },
119
+ cropAreaStyle: {
120
+ border: "2px solid #fff",
121
+ color: "rgba(255, 255, 255, 0.9)",
122
+ },
123
+ mediaStyle: {
124
+ backgroundColor: "#262626",
125
+ },
126
+ }}
127
+ />
128
+ </div>
129
+
130
+ <div className="flex items-center gap-4 px-1">
131
+ <ZoomOut className="h-4 w-4" />
132
+ <Slider
133
+ value={[zoom]}
134
+ onValueChange={(values: number[]) => setZoom(values[0])}
135
+ min={1}
136
+ max={3}
137
+ step={0.1}
138
+ className="flex-1"
139
+ />
140
+ <ZoomIn className="h-4 w-4" />
141
+ </div>
142
+
143
+ <div className="mt-4 flex justify-end space-x-2">
144
+ <Button variant="outline" onClick={onCancel}>
145
+ {t("web-ui:common.cancel")}
146
+ </Button>
147
+ <Button onClick={handleSave} disabled={isLoading}>
148
+ {isLoading ? t("web-ui:common.saving") : t("web-ui:common.save")}
149
+ </Button>
150
+ </div>
151
+ </DialogContent>
152
+ </Dialog>
153
+ );
154
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ Button,
3
+ type ButtonProps,
4
+ Modal,
5
+ ModalBody,
6
+ ModalContent,
7
+ ModalFooter,
8
+ ModalHeader,
9
+ } from "@heroui/react";
10
+ import { semanticColors } from "@heroui/theme";
11
+ import { createContext, useContext, useRef, useState } from "react";
12
+
13
+ export type DialogProps = {
14
+ title: React.ReactNode;
15
+ description: React.ReactNode;
16
+ color?: ButtonProps["color"];
17
+ cancelable?: boolean;
18
+ onCancel?: () => void;
19
+ onConfirm?: () => void;
20
+ cancelLabel?: string;
21
+ confirmLabel?: string;
22
+ };
23
+
24
+ const DialogContext = createContext<(dialog: DialogProps) => void>(() => {
25
+ console.warn("DialogProvider is not initialized");
26
+ });
27
+
28
+ export function useDialog() {
29
+ const context = useContext(DialogContext);
30
+ if (!context) {
31
+ throw new Error("useDialog must be used within a DialogProvider");
32
+ }
33
+ return context;
34
+ }
35
+
36
+ export function DialogProvider({ children }: { children: React.ReactNode }) {
37
+ const [isOpen, setIsOpen] = useState(false);
38
+ const [dialog, setDialog] = useState<DialogProps | null>(null);
39
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
40
+
41
+ const handleSetDialog = (dialog: DialogProps) => {
42
+ if (timeoutRef.current) {
43
+ clearTimeout(timeoutRef.current);
44
+ timeoutRef.current = null;
45
+ }
46
+ setDialog(dialog);
47
+ setIsOpen(true);
48
+ };
49
+
50
+ const handleUnsetDialog = () => {
51
+ setIsOpen(false);
52
+ if (timeoutRef.current) {
53
+ clearTimeout(timeoutRef.current);
54
+ timeoutRef.current = null;
55
+ }
56
+ timeoutRef.current = setTimeout(() => {
57
+ setDialog(null);
58
+ }, 500);
59
+ };
60
+
61
+ const handleCancel = () => {
62
+ dialog?.onCancel?.();
63
+ handleUnsetDialog();
64
+ };
65
+
66
+ const handleConfirm = () => {
67
+ dialog?.onConfirm?.();
68
+ handleUnsetDialog();
69
+ };
70
+
71
+ return (
72
+ <DialogContext.Provider value={handleSetDialog}>
73
+ <>
74
+ {children}
75
+ {dialog && (
76
+ <Modal
77
+ isOpen={isOpen}
78
+ onOpenChange={handleUnsetDialog}
79
+ style={{
80
+ borderColor: semanticColors.light[dialog.color || "danger"][600],
81
+ }}
82
+ classNames={{
83
+ base: "border-1",
84
+ }}
85
+ >
86
+ <ModalContent>
87
+ <ModalHeader>{dialog.title}</ModalHeader>
88
+ <ModalBody>{dialog.description}</ModalBody>
89
+ <ModalFooter>
90
+ <div className="flex flex-row gap-2">
91
+ {(dialog.cancelable || dialog.onCancel) && (
92
+ <Button onPress={handleCancel}>{dialog.cancelLabel ?? "Cancel"}</Button>
93
+ )}
94
+ <Button color={dialog.color} onPress={handleConfirm}>
95
+ {dialog.confirmLabel ?? "Confirm"}
96
+ </Button>
97
+ </div>
98
+ </ModalFooter>
99
+ </ModalContent>
100
+ </Modal>
101
+ )}
102
+ </>
103
+ </DialogContext.Provider>
104
+ );
105
+ }
@@ -0,0 +1,17 @@
1
+ export function ErrorFallback({
2
+ error,
3
+ componentStack,
4
+ }: {
5
+ error: unknown;
6
+ componentStack: string;
7
+ }) {
8
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
9
+ return (
10
+ <div className="flex flex-grow-1 flex-col items-center justify-center h-[100vh]">
11
+ <h1 className="text-3xl font-bold">Something went wrong.</h1>
12
+ <h1 className="text-2xl text-gray-700 font-bold mb-5">Please try again later.</h1>
13
+ <div className="text-lg text-gray-700 mb-5">{errorMessage}</div>
14
+ <div className="text-sm text-gray-500 px-10">{componentStack}</div>
15
+ </div>
16
+ );
17
+ }