@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.
- package/LICENSE +621 -0
- package/README.md +17 -0
- package/package.json +169 -0
- package/src/animations/card.motion.ts +9 -0
- package/src/components/AvatarUpload.tsx +133 -0
- package/src/components/Button.tsx +14 -0
- package/src/components/Calendar.css +684 -0
- package/src/components/Calendar.tsx +32 -0
- package/src/components/CardsSelect.tsx +155 -0
- package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
- package/src/components/ColorPicker.tsx +56 -0
- package/src/components/CopyButton.tsx +45 -0
- package/src/components/CropDialog.tsx +154 -0
- package/src/components/DialogProvider.tsx +105 -0
- package/src/components/ErrorFallback.tsx +17 -0
- package/src/components/FileDropzone.tsx +120 -0
- package/src/components/MultiSelectDropdown.tsx +233 -0
- package/src/components/Orb.tsx +288 -0
- package/src/components/PageAlert.tsx +121 -0
- package/src/components/SelectChips.tsx +40 -0
- package/src/components/SidebarItem.tsx +26 -0
- package/src/components/Steps.tsx +340 -0
- package/src/components/TablerIconPicker.tsx +4260 -0
- package/src/components/app-header.tsx +40 -0
- package/src/components/blur-card.tsx +132 -0
- package/src/components/features-section-demo-1.tsx +127 -0
- package/src/components/features-section-demo-2.tsx +102 -0
- package/src/components/features-section-demo-3.tsx +272 -0
- package/src/components/mode-toggle.tsx +31 -0
- package/src/components/nav-main.tsx +69 -0
- package/src/components/pricing-cards.tsx +133 -0
- package/src/components/shared/ButtonCopy.tsx +50 -0
- package/src/components/team-switcher.tsx +83 -0
- package/src/components/theme-provider.tsx +74 -0
- package/src/components/typewriter.tsx +90 -0
- package/src/components/ui/alert-dialog.tsx +133 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/bento-grid.tsx +54 -0
- package/src/components/ui/bento-grid2.tsx +66 -0
- package/src/components/ui/breadcrumb.tsx +101 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +55 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/dropdown-menu.tsx +186 -0
- package/src/components/ui/floating-navbar.tsx +78 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/image.tsx +55 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/pagination.tsx +105 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/resizable-navbar.tsx +260 -0
- package/src/components/ui/segment-control.tsx +143 -0
- package/src/components/ui/select.tsx +153 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +121 -0
- package/src/components/ui/sidebar.tsx +736 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/spinner.tsx +45 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +90 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/timeline.tsx +95 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/ui/typewriter-effect.tsx +181 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/useDialog.ts +25 -0
- package/src/icons/GoogleIcon.tsx +32 -0
- package/src/icons/LinkedInIcon.tsx +30 -0
- package/src/icons/MicrosoftIcon.tsx +21 -0
- package/src/lib/chatwoot.ts +51 -0
- package/src/lib/utils.ts +6 -0
- package/src/modules/app/components/AppLoader.tsx +9 -0
- package/src/modules/app/components/AppShell.tsx +21 -0
- package/src/modules/app/components/AppSidebar.tsx +26 -0
- package/src/modules/app/components/AppSidebarContent.tsx +73 -0
- package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
- package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
- package/src/modules/app/components/AppSidebarUser.tsx +128 -0
- package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
- package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
- package/src/modules/auth/components/AuthLayout.tsx +13 -0
- package/src/modules/auth/components/AuthProviders.tsx +105 -0
- package/src/modules/auth/components/AuthRouter.tsx +29 -0
- package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
- package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
- package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
- package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/InviteFriends.tsx +273 -0
- package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
- package/src/modules/auth/components/LoginForm.tsx +104 -0
- package/src/modules/auth/components/LoginRoute.tsx +31 -0
- package/src/modules/auth/components/LogoutRoute.tsx +21 -0
- package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
- package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
- package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
- package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
- package/src/modules/auth/components/ProfileRoute.tsx +104 -0
- package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
- package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
- package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
- package/src/modules/auth/components/SignupRoute.tsx +53 -0
- package/src/modules/auth/components/UserPreferences.tsx +144 -0
- package/src/modules/auth/components/WaitlistCard.tsx +78 -0
- package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
- package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
- package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
- package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
- package/src/modules/billing/components/BillingRouter.tsx +20 -0
- package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
- package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
- package/src/modules/table/components/NuqsTable.tsx +396 -0
- package/src/modules/table/components/TableFiltering.tsx +520 -0
- package/src/modules/table/components/TablePagination.tsx +59 -0
- package/src/modules/table/components/table.types.ts +11 -0
- package/src/modules/table/filterTransformers.ts +323 -0
- package/src/types.ts +4 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Dropdown,
|
|
4
|
+
DropdownItem,
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownTrigger,
|
|
7
|
+
Input,
|
|
8
|
+
Modal,
|
|
9
|
+
ModalBody,
|
|
10
|
+
ModalContent,
|
|
11
|
+
ModalFooter,
|
|
12
|
+
ModalHeader,
|
|
13
|
+
Spinner,
|
|
14
|
+
Table,
|
|
15
|
+
TableBody,
|
|
16
|
+
TableCell,
|
|
17
|
+
TableColumn,
|
|
18
|
+
TableHeader,
|
|
19
|
+
TableRow,
|
|
20
|
+
} from "@heroui/react";
|
|
21
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
22
|
+
import { Mail, MoreHorizontal, Search, Trash2, UserPlus, X } from "lucide-react";
|
|
23
|
+
import { useEffect, useId, useState } from "react";
|
|
24
|
+
import { toast } from "sonner";
|
|
25
|
+
import type { UseBackendTRPC } from "#types";
|
|
26
|
+
|
|
27
|
+
interface AdminWaitlistProps {
|
|
28
|
+
useTRPC: UseBackendTRPC;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AdminWaitlist({ useTRPC }: AdminWaitlistProps) {
|
|
32
|
+
const trpc = useTRPC();
|
|
33
|
+
const queryClient = useQueryClient();
|
|
34
|
+
|
|
35
|
+
const { data: waitlist = [], isLoading } = useQuery(trpc.auth.listAdminWaitlist.queryOptions());
|
|
36
|
+
const { mutate: invite, isPending: isInviting } = useMutation(
|
|
37
|
+
trpc.auth.inviteFromWaitlist.mutationOptions({
|
|
38
|
+
onSuccess: () => {
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: trpc.auth.listAdminWaitlist.queryKey() });
|
|
40
|
+
toast.success("Invitation sent successfully");
|
|
41
|
+
},
|
|
42
|
+
onError: (error) => {
|
|
43
|
+
toast.error(
|
|
44
|
+
`Failed to send invitation: ${error instanceof Error ? error.message : String(error)}`
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
const { mutate: remove, isPending: isRemoving } = useMutation(
|
|
50
|
+
trpc.auth.removeFromWaitlist.mutationOptions({
|
|
51
|
+
onSuccess: () => {
|
|
52
|
+
queryClient.invalidateQueries({ queryKey: trpc.auth.listAdminWaitlist.queryKey() });
|
|
53
|
+
toast.success("Removed from waitlist successfully");
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => {
|
|
56
|
+
toast.error(`Failed to remove: ${error instanceof Error ? error.message : String(error)}`);
|
|
57
|
+
},
|
|
58
|
+
onSettled: () => {
|
|
59
|
+
setItemToDelete(null);
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
const { mutate: add, isPending: isAdding } = useMutation(
|
|
64
|
+
trpc.auth.addToWaitlist.mutationOptions({
|
|
65
|
+
onSuccess: () => {
|
|
66
|
+
queryClient.invalidateQueries({ queryKey: trpc.auth.listAdminWaitlist.queryKey() });
|
|
67
|
+
toast.success("Added to waitlist successfully");
|
|
68
|
+
},
|
|
69
|
+
onError: (error) => {
|
|
70
|
+
toast.error(
|
|
71
|
+
`Failed to add to waitlist: ${error instanceof Error ? error.message : String(error)}`
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
const emailInputId = useId();
|
|
77
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
78
|
+
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
|
|
79
|
+
const [itemToDelete, setItemToDelete] = useState<string | null>(null);
|
|
80
|
+
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
81
|
+
const [newEmail, setNewEmail] = useState("");
|
|
82
|
+
|
|
83
|
+
// Debounce search query
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
setDebouncedSearchQuery(searchQuery);
|
|
87
|
+
}, 300);
|
|
88
|
+
|
|
89
|
+
return () => clearTimeout(timer);
|
|
90
|
+
}, [searchQuery]);
|
|
91
|
+
|
|
92
|
+
const filteredWaitlist = waitlist.filter((item) =>
|
|
93
|
+
item.email?.toLowerCase().includes(debouncedSearchQuery?.toLowerCase() ?? "")
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const handleInvite = (id: string) => {
|
|
97
|
+
invite({ id });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleRemove = () => {
|
|
101
|
+
if (itemToDelete) {
|
|
102
|
+
remove({ id: itemToDelete });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleAdd = async () => {
|
|
107
|
+
if (!newEmail.trim()) {
|
|
108
|
+
toast.error("Please enter an email address");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Basic email validation
|
|
113
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
114
|
+
if (!emailRegex.test(newEmail.trim())) {
|
|
115
|
+
toast.error("Please enter a valid email address");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
add({ email: newEmail.trim() });
|
|
120
|
+
setNewEmail("");
|
|
121
|
+
setIsAddModalOpen(false);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const formatDate = (date: Date | string | null) => {
|
|
125
|
+
if (!date) return "N/A";
|
|
126
|
+
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
127
|
+
return dateObj.toLocaleDateString(undefined, {
|
|
128
|
+
year: "numeric",
|
|
129
|
+
month: "short",
|
|
130
|
+
day: "numeric",
|
|
131
|
+
hour: "2-digit",
|
|
132
|
+
minute: "2-digit",
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getStatusBadge = (status: string) => {
|
|
137
|
+
const statusLower = status.toLowerCase();
|
|
138
|
+
if (statusLower === "invited" || statusLower === "active") {
|
|
139
|
+
return (
|
|
140
|
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
141
|
+
{status}
|
|
142
|
+
</span>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (statusLower === "pending") {
|
|
146
|
+
return (
|
|
147
|
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
148
|
+
{status}
|
|
149
|
+
</span>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
154
|
+
{status}
|
|
155
|
+
</span>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (isLoading) {
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex justify-center p-8">
|
|
162
|
+
<Spinner />
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="space-y-4 p-4">
|
|
169
|
+
<div className="flex justify-between items-center">
|
|
170
|
+
<h2 className="text-xl font-semibold">Waitlist Management</h2>
|
|
171
|
+
<div className="flex items-center gap-2">
|
|
172
|
+
<Button onPress={() => setIsAddModalOpen(true)} size="sm">
|
|
173
|
+
<UserPlus className="h-4 w-4 mr-2" />
|
|
174
|
+
Add to Waitlist
|
|
175
|
+
</Button>
|
|
176
|
+
<div className="text-sm text-muted-foreground">Total: {waitlist.length}</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* Search */}
|
|
181
|
+
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center">
|
|
182
|
+
<form
|
|
183
|
+
className="relative flex-1"
|
|
184
|
+
onSubmit={(e) => {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
setDebouncedSearchQuery(searchQuery);
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
190
|
+
<Input
|
|
191
|
+
aria-label="Search by email"
|
|
192
|
+
placeholder="Search by email..."
|
|
193
|
+
className="pl-8 w-full"
|
|
194
|
+
value={searchQuery}
|
|
195
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
196
|
+
variant="bordered"
|
|
197
|
+
/>
|
|
198
|
+
{searchQuery && (
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={() => setSearchQuery("")}
|
|
202
|
+
className="absolute right-2.5 top-2.5 text-muted-foreground hover:text-foreground"
|
|
203
|
+
aria-label="Clear search"
|
|
204
|
+
>
|
|
205
|
+
<X className="h-4 w-4" />
|
|
206
|
+
</button>
|
|
207
|
+
)}
|
|
208
|
+
</form>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div className="border rounded-lg overflow-hidden">
|
|
212
|
+
<Table aria-label="Waitlist table" removeWrapper>
|
|
213
|
+
<TableHeader>
|
|
214
|
+
<TableColumn>Email</TableColumn>
|
|
215
|
+
<TableColumn>Status</TableColumn>
|
|
216
|
+
<TableColumn>Created At</TableColumn>
|
|
217
|
+
<TableColumn>Updated At</TableColumn>
|
|
218
|
+
<TableColumn className="text-right">Actions</TableColumn>
|
|
219
|
+
</TableHeader>
|
|
220
|
+
<TableBody
|
|
221
|
+
items={filteredWaitlist}
|
|
222
|
+
emptyContent={
|
|
223
|
+
searchQuery
|
|
224
|
+
? "No waitlist entries found matching your search"
|
|
225
|
+
: "No waitlist entries found"
|
|
226
|
+
}
|
|
227
|
+
>
|
|
228
|
+
{(item) => (
|
|
229
|
+
<TableRow key={item.id}>
|
|
230
|
+
<TableCell className="font-medium">{item.email}</TableCell>
|
|
231
|
+
<TableCell>{getStatusBadge(item.status)}</TableCell>
|
|
232
|
+
<TableCell>{formatDate(item.createdAt)}</TableCell>
|
|
233
|
+
<TableCell>{formatDate(item.updatedAt)}</TableCell>
|
|
234
|
+
<TableCell className="text-right">
|
|
235
|
+
<Dropdown placement="bottom-end">
|
|
236
|
+
<DropdownTrigger>
|
|
237
|
+
<Button variant="light" size="sm" isIconOnly>
|
|
238
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
239
|
+
</Button>
|
|
240
|
+
</DropdownTrigger>
|
|
241
|
+
<DropdownMenu aria-label="Waitlist actions">
|
|
242
|
+
<DropdownItem
|
|
243
|
+
key="invite"
|
|
244
|
+
onPress={() => handleInvite(item.id)}
|
|
245
|
+
isDisabled={isInviting}
|
|
246
|
+
>
|
|
247
|
+
{isInviting ? (
|
|
248
|
+
<>
|
|
249
|
+
<Spinner className="mr-2 h-3 w-3" />
|
|
250
|
+
Inviting...
|
|
251
|
+
</>
|
|
252
|
+
) : (
|
|
253
|
+
<>
|
|
254
|
+
<Mail className="mr-2 h-4 w-4" />
|
|
255
|
+
Send Invitation
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
258
|
+
</DropdownItem>
|
|
259
|
+
<DropdownItem
|
|
260
|
+
key="remove"
|
|
261
|
+
className="text-danger"
|
|
262
|
+
onClick={() => setItemToDelete(item.id)}
|
|
263
|
+
>
|
|
264
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
265
|
+
Remove
|
|
266
|
+
</DropdownItem>
|
|
267
|
+
</DropdownMenu>
|
|
268
|
+
</Dropdown>
|
|
269
|
+
</TableCell>
|
|
270
|
+
</TableRow>
|
|
271
|
+
)}
|
|
272
|
+
</TableBody>
|
|
273
|
+
</Table>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Remove confirmation modal */}
|
|
277
|
+
<Modal
|
|
278
|
+
isOpen={!!itemToDelete}
|
|
279
|
+
onOpenChange={(open) => {
|
|
280
|
+
if (!open) setItemToDelete(null);
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<ModalContent>
|
|
284
|
+
{(onClose) => (
|
|
285
|
+
<>
|
|
286
|
+
<ModalHeader className="flex flex-col gap-1">
|
|
287
|
+
<p className="text-lg font-semibold">Are you sure?</p>
|
|
288
|
+
<p className="text-sm text-default-600">
|
|
289
|
+
This action cannot be undone. This will permanently remove this entry from the
|
|
290
|
+
waitlist.
|
|
291
|
+
</p>
|
|
292
|
+
</ModalHeader>
|
|
293
|
+
<ModalFooter>
|
|
294
|
+
<Button variant="bordered" onPress={onClose}>
|
|
295
|
+
Cancel
|
|
296
|
+
</Button>
|
|
297
|
+
<Button color="danger" onPress={handleRemove} isLoading={isRemoving}>
|
|
298
|
+
Remove
|
|
299
|
+
</Button>
|
|
300
|
+
</ModalFooter>
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</ModalContent>
|
|
304
|
+
</Modal>
|
|
305
|
+
|
|
306
|
+
{/* Add to waitlist modal */}
|
|
307
|
+
<Modal
|
|
308
|
+
isOpen={isAddModalOpen}
|
|
309
|
+
onOpenChange={(open) => {
|
|
310
|
+
if (!open) setIsAddModalOpen(false);
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
<ModalContent>
|
|
314
|
+
{(onClose) => (
|
|
315
|
+
<form
|
|
316
|
+
onSubmit={(e) => {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
handleAdd();
|
|
319
|
+
}}
|
|
320
|
+
className="space-y-4"
|
|
321
|
+
>
|
|
322
|
+
<ModalHeader className="flex flex-col gap-1">
|
|
323
|
+
<p className="text-lg font-semibold">Add to Waitlist</p>
|
|
324
|
+
<p className="text-sm text-default-600">
|
|
325
|
+
Enter an email address to add someone to the waitlist.
|
|
326
|
+
</p>
|
|
327
|
+
</ModalHeader>
|
|
328
|
+
|
|
329
|
+
<ModalBody className="space-y-4">
|
|
330
|
+
<div className="space-y-2">
|
|
331
|
+
<Input
|
|
332
|
+
id={emailInputId}
|
|
333
|
+
label="Email *"
|
|
334
|
+
labelPlacement="outside"
|
|
335
|
+
type="email"
|
|
336
|
+
placeholder="Enter email address"
|
|
337
|
+
value={newEmail}
|
|
338
|
+
onChange={(e) => setNewEmail(e.target.value)}
|
|
339
|
+
variant="bordered"
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
</ModalBody>
|
|
343
|
+
|
|
344
|
+
<ModalFooter>
|
|
345
|
+
<Button variant="bordered" type="button" onPress={onClose}>
|
|
346
|
+
Cancel
|
|
347
|
+
</Button>
|
|
348
|
+
<Button type="submit" color="primary" isDisabled={isAdding} isLoading={isAdding}>
|
|
349
|
+
{isAdding ? "Adding..." : "Add to Waitlist"}
|
|
350
|
+
</Button>
|
|
351
|
+
</ModalFooter>
|
|
352
|
+
</form>
|
|
353
|
+
)}
|
|
354
|
+
</ModalContent>
|
|
355
|
+
</Modal>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Outlet } from "react-router";
|
|
3
|
+
|
|
4
|
+
export function AuthLayout({ header }: { header: ReactNode }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
7
|
+
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
8
|
+
<div className="flex items-center gap-2 self-center font-medium">{header}</div>
|
|
9
|
+
<Outlet />
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Button } from "@heroui/react";
|
|
2
|
+
import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { GoogleIcon } from "#icons/GoogleIcon";
|
|
6
|
+
import { LinkedInIcon } from "#icons/LinkedInIcon";
|
|
7
|
+
import { MicrosoftIcon } from "#icons/MicrosoftIcon";
|
|
8
|
+
import { LastUsedBadge } from "./LastUsedBadge";
|
|
9
|
+
|
|
10
|
+
export function AuthProviders({
|
|
11
|
+
providers,
|
|
12
|
+
lastMethod,
|
|
13
|
+
code,
|
|
14
|
+
requestSignUp = false,
|
|
15
|
+
}: {
|
|
16
|
+
providers?: string[];
|
|
17
|
+
code?: string | null;
|
|
18
|
+
requestSignUp?: boolean;
|
|
19
|
+
lastMethod?: string | null;
|
|
20
|
+
}) {
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
if (!providers || providers.length === 0) return null;
|
|
23
|
+
const additionalData = code ? { waitlistInvitationCode: code } : {};
|
|
24
|
+
|
|
25
|
+
const handleSignIn = (result: any) => {
|
|
26
|
+
if (result.error) {
|
|
27
|
+
toast.error(t("web-ui:auth.errors.invitationCodeInvalid"));
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<div className="flex flex-col gap-4">
|
|
34
|
+
{providers.includes("google") && (
|
|
35
|
+
<LastUsedBadge lastMethod={lastMethod} method="google">
|
|
36
|
+
<Button
|
|
37
|
+
type="button"
|
|
38
|
+
variant="bordered"
|
|
39
|
+
className="w-full gap-2"
|
|
40
|
+
onPress={() => {
|
|
41
|
+
authClient.signIn
|
|
42
|
+
.social({
|
|
43
|
+
provider: "google",
|
|
44
|
+
requestSignUp,
|
|
45
|
+
additionalData,
|
|
46
|
+
})
|
|
47
|
+
.then(handleSignIn);
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<GoogleIcon className="h-5 w-5" />
|
|
51
|
+
{t("web-ui:auth.login.google")}
|
|
52
|
+
</Button>
|
|
53
|
+
</LastUsedBadge>
|
|
54
|
+
)}
|
|
55
|
+
{providers.includes("linkedin") && (
|
|
56
|
+
<LastUsedBadge lastMethod={lastMethod} method="linkedin">
|
|
57
|
+
<Button
|
|
58
|
+
type="button"
|
|
59
|
+
variant="bordered"
|
|
60
|
+
className="w-full"
|
|
61
|
+
onPress={() => {
|
|
62
|
+
authClient.signIn
|
|
63
|
+
.social({
|
|
64
|
+
provider: "linkedin",
|
|
65
|
+
requestSignUp,
|
|
66
|
+
additionalData,
|
|
67
|
+
})
|
|
68
|
+
.then(handleSignIn);
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<LinkedInIcon className="h-5 w-5" />
|
|
72
|
+
{t("web-ui:auth.login.linkedin")}
|
|
73
|
+
</Button>
|
|
74
|
+
</LastUsedBadge>
|
|
75
|
+
)}
|
|
76
|
+
{providers.includes("microsoft") && (
|
|
77
|
+
<LastUsedBadge lastMethod={lastMethod} method="microsoft">
|
|
78
|
+
<Button
|
|
79
|
+
type="button"
|
|
80
|
+
variant="bordered"
|
|
81
|
+
className="w-full"
|
|
82
|
+
onPress={() => {
|
|
83
|
+
authClient.signIn
|
|
84
|
+
.social({
|
|
85
|
+
provider: "microsoft",
|
|
86
|
+
requestSignUp,
|
|
87
|
+
additionalData,
|
|
88
|
+
})
|
|
89
|
+
.then(handleSignIn);
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<MicrosoftIcon className="h-5 w-5" />
|
|
93
|
+
{t("web-ui:auth.login.microsoft")}
|
|
94
|
+
</Button>
|
|
95
|
+
</LastUsedBadge>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
|
99
|
+
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
|
100
|
+
{t("web-ui:auth.login.orContinueWith")}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Route } from "react-router";
|
|
3
|
+
import { AuthLayout } from "#modules/auth/components/AuthLayout";
|
|
4
|
+
import { ClaimAccountRoute } from "#modules/auth/components/ClaimAccountRoute";
|
|
5
|
+
import { ErrorAuthRoute } from "#modules/auth/components/ErrorAuthRoute";
|
|
6
|
+
import { ForgotPasswordRoute } from "#modules/auth/components/ForgotPasswordRoute";
|
|
7
|
+
import { LoginRoute } from "#modules/auth/components/LoginRoute";
|
|
8
|
+
import { ResetPasswordRoute } from "#modules/auth/components/ResetPasswordRoute";
|
|
9
|
+
import { SignupRoute } from "#modules/auth/components/SignupRoute";
|
|
10
|
+
import type { UseBackendTRPC } from "#types";
|
|
11
|
+
|
|
12
|
+
interface AuthRouterProps {
|
|
13
|
+
header: ReactNode;
|
|
14
|
+
providers?: string[];
|
|
15
|
+
useTRPC?: UseBackendTRPC;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AuthRouter({ header, providers, useTRPC }: AuthRouterProps) {
|
|
19
|
+
return (
|
|
20
|
+
<Route element={<AuthLayout header={header} />}>
|
|
21
|
+
<Route path="/login" element={<LoginRoute providers={providers} />} />
|
|
22
|
+
<Route path="/signup" element={<SignupRoute providers={providers} useTRPC={useTRPC} />} />
|
|
23
|
+
<Route path="/forgot-password" element={<ForgotPasswordRoute />} />
|
|
24
|
+
<Route path="/reset-password" element={<ResetPasswordRoute />} />
|
|
25
|
+
<Route path="/claim-account" element={<ClaimAccountRoute useTRPC={useTRPC} />} />
|
|
26
|
+
<Route path="/error-auth" element={<ErrorAuthRoute />} />
|
|
27
|
+
</Route>
|
|
28
|
+
);
|
|
29
|
+
}
|