create-nala 3.0.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 +21 -0
- package/README.md +58 -0
- package/dist/index.js +227 -0
- package/package.json +48 -0
- package/template/.env.example +45 -0
- package/template/_gitignore +33 -0
- package/template/components.json +24 -0
- package/template/index.html +30 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +15 -0
- package/template/public/icons.svg +24 -0
- package/template/public/img/avatar.webp +0 -0
- package/template/public/screenshot.png +0 -0
- package/template/src/App.vue +15 -0
- package/template/src/auto-imports.d.ts +691 -0
- package/template/src/components/AppLogo.vue +159 -0
- package/template/src/components/EmptyState.vue +60 -0
- package/template/src/components/PageHeader.vue +60 -0
- package/template/src/components/layout/AppNavbar.vue +159 -0
- package/template/src/components/layout/AppSidebar.vue +74 -0
- package/template/src/components/layout/CommandSearchDialog.vue +120 -0
- package/template/src/components/ui/accordion/Accordion.vue +15 -0
- package/template/src/components/ui/accordion/AccordionContent.vue +20 -0
- package/template/src/components/ui/accordion/AccordionItem.vue +20 -0
- package/template/src/components/ui/accordion/AccordionTrigger.vue +34 -0
- package/template/src/components/ui/accordion/index.ts +4 -0
- package/template/src/components/ui/alert/Alert.vue +21 -0
- package/template/src/components/ui/alert/AlertDescription.vue +17 -0
- package/template/src/components/ui/alert/AlertTitle.vue +17 -0
- package/template/src/components/ui/alert/index.ts +30 -0
- package/template/src/components/ui/alert-dialog/AlertDialog.vue +15 -0
- package/template/src/components/ui/alert-dialog/AlertDialogAction.vue +18 -0
- package/template/src/components/ui/alert-dialog/AlertDialogCancel.vue +25 -0
- package/template/src/components/ui/alert-dialog/AlertDialogContent.vue +44 -0
- package/template/src/components/ui/alert-dialog/AlertDialogDescription.vue +23 -0
- package/template/src/components/ui/alert-dialog/AlertDialogFooter.vue +22 -0
- package/template/src/components/ui/alert-dialog/AlertDialogHeader.vue +17 -0
- package/template/src/components/ui/alert-dialog/AlertDialogTitle.vue +21 -0
- package/template/src/components/ui/alert-dialog/AlertDialogTrigger.vue +12 -0
- package/template/src/components/ui/alert-dialog/index.ts +9 -0
- package/template/src/components/ui/avatar/Avatar.vue +50 -0
- package/template/src/components/ui/avatar/AvatarFallback.vue +21 -0
- package/template/src/components/ui/avatar/AvatarGroup.vue +69 -0
- package/template/src/components/ui/avatar/AvatarImage.vue +16 -0
- package/template/src/components/ui/avatar/index.ts +4 -0
- package/template/src/components/ui/badge/Badge.vue +40 -0
- package/template/src/components/ui/badge/index.ts +45 -0
- package/template/src/components/ui/breadcrumb/Breadcrumb.vue +17 -0
- package/template/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +23 -0
- package/template/src/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
- package/template/src/components/ui/breadcrumb/BreadcrumbLink.vue +21 -0
- package/template/src/components/ui/breadcrumb/BreadcrumbList.vue +17 -0
- package/template/src/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
- package/template/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +22 -0
- package/template/src/components/ui/breadcrumb/index.ts +7 -0
- package/template/src/components/ui/button/Button.spec.ts +46 -0
- package/template/src/components/ui/button/Button.vue +31 -0
- package/template/src/components/ui/button/index.ts +41 -0
- package/template/src/components/ui/calendar/Calendar.vue +106 -0
- package/template/src/components/ui/calendar/index.ts +1 -0
- package/template/src/components/ui/card/Card.vue +29 -0
- package/template/src/components/ui/card/CardAction.vue +17 -0
- package/template/src/components/ui/card/CardContent.vue +17 -0
- package/template/src/components/ui/card/CardDescription.vue +17 -0
- package/template/src/components/ui/card/CardFooter.vue +17 -0
- package/template/src/components/ui/card/CardHeader.vue +29 -0
- package/template/src/components/ui/card/CardTitle.vue +17 -0
- package/template/src/components/ui/card/index.ts +7 -0
- package/template/src/components/ui/chart/AreaChart.vue +143 -0
- package/template/src/components/ui/chart/BarChart.vue +142 -0
- package/template/src/components/ui/chart/DonutChart.vue +75 -0
- package/template/src/components/ui/chart/LineChart.vue +132 -0
- package/template/src/components/ui/chart/index.ts +4 -0
- package/template/src/components/ui/checkbox/Checkbox.vue +57 -0
- package/template/src/components/ui/checkbox/index.ts +1 -0
- package/template/src/components/ui/collapsible/Collapsible.vue +15 -0
- package/template/src/components/ui/collapsible/CollapsibleContent.vue +18 -0
- package/template/src/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
- package/template/src/components/ui/collapsible/index.ts +3 -0
- package/template/src/components/ui/command/Command.vue +88 -0
- package/template/src/components/ui/command/CommandDialog.vue +31 -0
- package/template/src/components/ui/command/CommandEmpty.vue +27 -0
- package/template/src/components/ui/command/CommandGroup.vue +45 -0
- package/template/src/components/ui/command/CommandInput.vue +39 -0
- package/template/src/components/ui/command/CommandItem.vue +76 -0
- package/template/src/components/ui/command/CommandList.vue +25 -0
- package/template/src/components/ui/command/CommandSeparator.vue +21 -0
- package/template/src/components/ui/command/CommandShortcut.vue +17 -0
- package/template/src/components/ui/command/index.ts +25 -0
- package/template/src/components/ui/context-menu/ContextMenu.vue +15 -0
- package/template/src/components/ui/context-menu/ContextMenuCheckboxItem.vue +36 -0
- package/template/src/components/ui/context-menu/ContextMenuContent.vue +33 -0
- package/template/src/components/ui/context-menu/ContextMenuGroup.vue +12 -0
- package/template/src/components/ui/context-menu/ContextMenuItem.vue +36 -0
- package/template/src/components/ui/context-menu/ContextMenuLabel.vue +25 -0
- package/template/src/components/ui/context-menu/ContextMenuRadioGroup.vue +18 -0
- package/template/src/components/ui/context-menu/ContextMenuRadioItem.vue +36 -0
- package/template/src/components/ui/context-menu/ContextMenuSeparator.vue +18 -0
- package/template/src/components/ui/context-menu/ContextMenuShortcut.vue +17 -0
- package/template/src/components/ui/context-menu/ContextMenuSub.vue +15 -0
- package/template/src/components/ui/context-menu/ContextMenuSubContent.vue +36 -0
- package/template/src/components/ui/context-menu/ContextMenuSubTrigger.vue +32 -0
- package/template/src/components/ui/context-menu/ContextMenuTrigger.vue +13 -0
- package/template/src/components/ui/context-menu/index.ts +14 -0
- package/template/src/components/ui/date-picker/DatePicker.vue +233 -0
- package/template/src/components/ui/date-picker/index.ts +1 -0
- package/template/src/components/ui/dialog/Dialog.vue +19 -0
- package/template/src/components/ui/dialog/DialogClose.vue +15 -0
- package/template/src/components/ui/dialog/DialogContent.vue +53 -0
- package/template/src/components/ui/dialog/DialogDescription.vue +23 -0
- package/template/src/components/ui/dialog/DialogFooter.vue +27 -0
- package/template/src/components/ui/dialog/DialogHeader.vue +17 -0
- package/template/src/components/ui/dialog/DialogOverlay.vue +21 -0
- package/template/src/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/template/src/components/ui/dialog/DialogTitle.vue +23 -0
- package/template/src/components/ui/dialog/DialogTrigger.vue +15 -0
- package/template/src/components/ui/dialog/index.ts +10 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuContent.vue +41 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
- package/template/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
- package/template/src/components/ui/dropdown-menu/index.ts +16 -0
- package/template/src/components/ui/form/FormControl.vue +17 -0
- package/template/src/components/ui/form/FormDescription.vue +21 -0
- package/template/src/components/ui/form/FormItem.vue +23 -0
- package/template/src/components/ui/form/FormLabel.vue +25 -0
- package/template/src/components/ui/form/FormMessage.vue +23 -0
- package/template/src/components/ui/form/index.ts +7 -0
- package/template/src/components/ui/form/injectionKeys.ts +4 -0
- package/template/src/components/ui/form/useFormField.ts +30 -0
- package/template/src/components/ui/hover-card/HoverCard.vue +19 -0
- package/template/src/components/ui/hover-card/HoverCardContent.vue +43 -0
- package/template/src/components/ui/hover-card/HoverCardTrigger.vue +15 -0
- package/template/src/components/ui/hover-card/index.ts +3 -0
- package/template/src/components/ui/index.ts +47 -0
- package/template/src/components/ui/input/Input.vue +33 -0
- package/template/src/components/ui/input/InputAddon.vue +29 -0
- package/template/src/components/ui/input/InputGroup.vue +17 -0
- package/template/src/components/ui/input/InputIcon.vue +29 -0
- package/template/src/components/ui/input/index.ts +4 -0
- package/template/src/components/ui/kbd/Kbd.vue +30 -0
- package/template/src/components/ui/kbd/index.ts +1 -0
- package/template/src/components/ui/label/Label.vue +26 -0
- package/template/src/components/ui/label/index.ts +1 -0
- package/template/src/components/ui/loading-bar/LoadingBar.vue +41 -0
- package/template/src/components/ui/loading-bar/index.ts +2 -0
- package/template/src/components/ui/pagination/Pagination.vue +26 -0
- package/template/src/components/ui/pagination/PaginationContent.vue +22 -0
- package/template/src/components/ui/pagination/PaginationEllipsis.vue +25 -0
- package/template/src/components/ui/pagination/PaginationFirst.vue +33 -0
- package/template/src/components/ui/pagination/PaginationItem.vue +34 -0
- package/template/src/components/ui/pagination/PaginationLast.vue +33 -0
- package/template/src/components/ui/pagination/PaginationNext.vue +33 -0
- package/template/src/components/ui/pagination/PaginationPrevious.vue +33 -0
- package/template/src/components/ui/pagination/index.ts +8 -0
- package/template/src/components/ui/pin-input/PinInput.vue +28 -0
- package/template/src/components/ui/pin-input/PinInputGroup.vue +18 -0
- package/template/src/components/ui/pin-input/PinInputInput.vue +24 -0
- package/template/src/components/ui/pin-input/PinInputSeparator.vue +21 -0
- package/template/src/components/ui/pin-input/index.ts +4 -0
- package/template/src/components/ui/popover/Popover.vue +19 -0
- package/template/src/components/ui/popover/PopoverAnchor.vue +15 -0
- package/template/src/components/ui/popover/PopoverContent.vue +45 -0
- package/template/src/components/ui/popover/PopoverTrigger.vue +15 -0
- package/template/src/components/ui/popover/index.ts +4 -0
- package/template/src/components/ui/progress/Progress.vue +35 -0
- package/template/src/components/ui/progress/index.ts +1 -0
- package/template/src/components/ui/radio-group/RadioGroup.vue +23 -0
- package/template/src/components/ui/radio-group/RadioGroupItem.vue +30 -0
- package/template/src/components/ui/radio-group/index.ts +2 -0
- package/template/src/components/ui/scroll-area/ScrollArea.vue +40 -0
- package/template/src/components/ui/scroll-area/ScrollBar.vue +44 -0
- package/template/src/components/ui/scroll-area/index.ts +2 -0
- package/template/src/components/ui/select/Select.vue +19 -0
- package/template/src/components/ui/select/SelectContent.vue +51 -0
- package/template/src/components/ui/select/SelectGroup.vue +15 -0
- package/template/src/components/ui/select/SelectItem.vue +44 -0
- package/template/src/components/ui/select/SelectItemText.vue +15 -0
- package/template/src/components/ui/select/SelectLabel.vue +17 -0
- package/template/src/components/ui/select/SelectScrollDownButton.vue +26 -0
- package/template/src/components/ui/select/SelectScrollUpButton.vue +26 -0
- package/template/src/components/ui/select/SelectSeparator.vue +19 -0
- package/template/src/components/ui/select/SelectTrigger.vue +33 -0
- package/template/src/components/ui/select/SelectValue.vue +15 -0
- package/template/src/components/ui/select/index.ts +11 -0
- package/template/src/components/ui/separator/Separator.vue +29 -0
- package/template/src/components/ui/separator/index.ts +1 -0
- package/template/src/components/ui/sheet/Sheet.vue +19 -0
- package/template/src/components/ui/sheet/SheetClose.vue +15 -0
- package/template/src/components/ui/sheet/SheetContent.vue +62 -0
- package/template/src/components/ui/sheet/SheetDescription.vue +21 -0
- package/template/src/components/ui/sheet/SheetFooter.vue +16 -0
- package/template/src/components/ui/sheet/SheetHeader.vue +15 -0
- package/template/src/components/ui/sheet/SheetOverlay.vue +21 -0
- package/template/src/components/ui/sheet/SheetTitle.vue +21 -0
- package/template/src/components/ui/sheet/SheetTrigger.vue +15 -0
- package/template/src/components/ui/sheet/index.ts +8 -0
- package/template/src/components/ui/sidebar/Sidebar.vue +113 -0
- package/template/src/components/ui/sidebar/SidebarContent.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarControl.vue +53 -0
- package/template/src/components/ui/sidebar/SidebarFooter.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarGroup.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarGroupAction.vue +27 -0
- package/template/src/components/ui/sidebar/SidebarGroupContent.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarGroupLabel.vue +25 -0
- package/template/src/components/ui/sidebar/SidebarHeader.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarInput.vue +22 -0
- package/template/src/components/ui/sidebar/SidebarInset.vue +21 -0
- package/template/src/components/ui/sidebar/SidebarMenu.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarMenuAction.vue +35 -0
- package/template/src/components/ui/sidebar/SidebarMenuBadge.vue +26 -0
- package/template/src/components/ui/sidebar/SidebarMenuButton.vue +48 -0
- package/template/src/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
- package/template/src/components/ui/sidebar/SidebarMenuItem.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
- package/template/src/components/ui/sidebar/SidebarMenuSub.vue +22 -0
- package/template/src/components/ui/sidebar/SidebarMenuSubButton.vue +36 -0
- package/template/src/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
- package/template/src/components/ui/sidebar/SidebarProvider.vue +136 -0
- package/template/src/components/ui/sidebar/SidebarRail.vue +33 -0
- package/template/src/components/ui/sidebar/SidebarSeparator.vue +19 -0
- package/template/src/components/ui/sidebar/SidebarTrigger.vue +27 -0
- package/template/src/components/ui/sidebar/index.ts +61 -0
- package/template/src/components/ui/sidebar/utils.ts +27 -0
- package/template/src/components/ui/skeleton/Skeleton.vue +17 -0
- package/template/src/components/ui/skeleton/index.ts +1 -0
- package/template/src/components/ui/slider/Slider.vue +42 -0
- package/template/src/components/ui/slider/index.ts +1 -0
- package/template/src/components/ui/sonner/Sonner.vue +42 -0
- package/template/src/components/ui/sonner/index.ts +2 -0
- package/template/src/components/ui/stepper/Stepper.vue +102 -0
- package/template/src/components/ui/stepper/index.ts +1 -0
- package/template/src/components/ui/switch/Switch.vue +38 -0
- package/template/src/components/ui/switch/index.ts +1 -0
- package/template/src/components/ui/table/Table.vue +16 -0
- package/template/src/components/ui/table/TableBody.vue +17 -0
- package/template/src/components/ui/table/TableCaption.vue +17 -0
- package/template/src/components/ui/table/TableCell.vue +22 -0
- package/template/src/components/ui/table/TableEmpty.vue +34 -0
- package/template/src/components/ui/table/TableFooter.vue +17 -0
- package/template/src/components/ui/table/TableHead.vue +17 -0
- package/template/src/components/ui/table/TableHeader.vue +17 -0
- package/template/src/components/ui/table/TableRow.vue +17 -0
- package/template/src/components/ui/table/index.ts +9 -0
- package/template/src/components/ui/table/utils.ts +10 -0
- package/template/src/components/ui/tabs/Tabs.vue +24 -0
- package/template/src/components/ui/tabs/TabsContent.vue +21 -0
- package/template/src/components/ui/tabs/TabsList.vue +24 -0
- package/template/src/components/ui/tabs/TabsTrigger.vue +26 -0
- package/template/src/components/ui/tabs/index.ts +4 -0
- package/template/src/components/ui/textarea/Textarea.vue +28 -0
- package/template/src/components/ui/textarea/index.ts +1 -0
- package/template/src/components/ui/timeline/Timeline.vue +73 -0
- package/template/src/components/ui/timeline/index.ts +1 -0
- package/template/src/components/ui/toggle/Toggle.vue +35 -0
- package/template/src/components/ui/toggle/index.ts +28 -0
- package/template/src/components/ui/toggle-group/ToggleGroup.vue +41 -0
- package/template/src/components/ui/toggle-group/ToggleGroupItem.vue +43 -0
- package/template/src/components/ui/toggle-group/index.ts +12 -0
- package/template/src/components/ui/tooltip/Tooltip.vue +19 -0
- package/template/src/components/ui/tooltip/TooltipContent.vue +34 -0
- package/template/src/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/template/src/components/ui/tooltip/TooltipTrigger.vue +15 -0
- package/template/src/components/ui/tooltip/index.ts +4 -0
- package/template/src/components.d.ts +228 -0
- package/template/src/composables/useFormatter.ts +105 -0
- package/template/src/composables/useThemeConfig.ts +252 -0
- package/template/src/layouts/AdminLayout.vue +55 -0
- package/template/src/layouts/AuthLayout.vue +42 -0
- package/template/src/lib/axios.spec.ts +46 -0
- package/template/src/lib/axios.ts +243 -0
- package/template/src/lib/formatters.spec.ts +137 -0
- package/template/src/lib/formatters.ts +332 -0
- package/template/src/lib/loading-bar.ts +103 -0
- package/template/src/lib/utils.spec.ts +26 -0
- package/template/src/lib/utils.ts +7 -0
- package/template/src/lib/validation.spec.ts +78 -0
- package/template/src/main.ts +13 -0
- package/template/src/router/index.ts +147 -0
- package/template/src/services/auth.service.spec.ts +93 -0
- package/template/src/services/auth.service.ts +243 -0
- package/template/src/services/index.ts +3 -0
- package/template/src/services/types.ts +28 -0
- package/template/src/services/user.service.spec.ts +91 -0
- package/template/src/services/user.service.ts +283 -0
- package/template/src/stores/auth.spec.ts +52 -0
- package/template/src/stores/auth.ts +130 -0
- package/template/src/style.css +587 -0
- package/template/src/views/_starter/BlankView.vue +47 -0
- package/template/src/views/auth/ConfirmEmailView.vue +209 -0
- package/template/src/views/auth/ForgotPasswordView.vue +151 -0
- package/template/src/views/auth/LockScreenView.vue +175 -0
- package/template/src/views/auth/LoginView.vue +54 -0
- package/template/src/views/auth/RegisterView.vue +265 -0
- package/template/src/views/auth/ResetPasswordView.vue +260 -0
- package/template/src/views/auth/VerifyOtpView.vue +261 -0
- package/template/src/views/dashboard/IndexView.vue +159 -0
- package/template/src/views/errors/ComingSoonView.vue +241 -0
- package/template/src/views/errors/MaintenanceView.vue +252 -0
- package/template/src/views/errors/NotFoundView.vue +81 -0
- package/template/src/views/errors/ServerErrorView.vue +101 -0
- package/template/src/views/errors/UnauthorizedView.vue +64 -0
- package/template/tsconfig.app.json +27 -0
- package/template/tsconfig.json +19 -0
- package/template/tsconfig.node.json +23 -0
- package/template/vite.config.ts +46 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Data Formatting Utilities
|
|
3
|
+
* Uses native browser Intl APIs for optimal performance without external libraries.
|
|
4
|
+
*
|
|
5
|
+
* Global defaults can be configured via environment variables:
|
|
6
|
+
* - VITE_DEFAULT_LOCALE (e.g. "en-US", "id-ID", "de-DE") → default: "en-US"
|
|
7
|
+
* - VITE_DEFAULT_CURRENCY (e.g. "USD", "IDR", "EUR") → default: "USD"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Global locale — override via VITE_DEFAULT_LOCALE in .env */
|
|
11
|
+
const DEFAULT_LOCALE: string = import.meta.env.VITE_DEFAULT_LOCALE ?? 'en-US'
|
|
12
|
+
|
|
13
|
+
/** Global currency — override via VITE_DEFAULT_CURRENCY in .env */
|
|
14
|
+
const DEFAULT_CURRENCY: string = import.meta.env.VITE_DEFAULT_CURRENCY ?? 'USD'
|
|
15
|
+
|
|
16
|
+
/** Global timezone — override via VITE_DEFAULT_TIMEZONE in .env (e.g. "Asia/Jakarta") */
|
|
17
|
+
const DEFAULT_TIMEZONE: string = import.meta.env.VITE_DEFAULT_TIMEZONE ?? 'Asia/Jakarta'
|
|
18
|
+
|
|
19
|
+
/** Global time format — override via VITE_DEFAULT_TIME_FORMAT in .env ('24h' | '12h' | 'auto') */
|
|
20
|
+
export type TimeFormat = '24h' | '12h' | 'auto'
|
|
21
|
+
const DEFAULT_TIME_FORMAT: TimeFormat = (import.meta.env.VITE_DEFAULT_TIME_FORMAT as TimeFormat) ?? '24h'
|
|
22
|
+
|
|
23
|
+
export interface CurrencyFormatOptions {
|
|
24
|
+
currency?: string
|
|
25
|
+
locale?: string
|
|
26
|
+
minimumFractionDigits?: number
|
|
27
|
+
maximumFractionDigits?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DateTimeFormatOptions {
|
|
31
|
+
locale?: string
|
|
32
|
+
timeZone?: string
|
|
33
|
+
format?: TimeFormat
|
|
34
|
+
dateStyle?: 'short' | 'medium' | 'long' | 'full'
|
|
35
|
+
timeStyle?: 'short' | 'medium' | 'long'
|
|
36
|
+
showSeconds?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TimeOnlyFormatOptions {
|
|
40
|
+
locale?: string
|
|
41
|
+
timeZone?: string
|
|
42
|
+
format?: TimeFormat
|
|
43
|
+
showSeconds?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format numeric value to localized currency
|
|
48
|
+
* @example formatCurrency(1500, { currency: 'USD' }) => "$1,500.00"
|
|
49
|
+
* @example formatCurrency(50000, { currency: 'IDR', locale: 'id-ID' }) => "Rp 50.000"
|
|
50
|
+
*/
|
|
51
|
+
export function formatCurrency(
|
|
52
|
+
value: number | string | null | undefined,
|
|
53
|
+
options: CurrencyFormatOptions = {},
|
|
54
|
+
): string {
|
|
55
|
+
if (value === null || value === undefined || value === '') return '-'
|
|
56
|
+
const numeric = typeof value === 'string' ? Number(value) : value
|
|
57
|
+
if (Number.isNaN(numeric)) return '-'
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
currency = DEFAULT_CURRENCY,
|
|
61
|
+
locale = DEFAULT_LOCALE,
|
|
62
|
+
minimumFractionDigits = currency === 'IDR' ? 0 : 2,
|
|
63
|
+
maximumFractionDigits = currency === 'IDR' ? 0 : 2,
|
|
64
|
+
} = options
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return new Intl.NumberFormat(locale, {
|
|
68
|
+
style: 'currency',
|
|
69
|
+
currency,
|
|
70
|
+
minimumFractionDigits,
|
|
71
|
+
maximumFractionDigits,
|
|
72
|
+
}).format(numeric)
|
|
73
|
+
} catch {
|
|
74
|
+
return `${currency} ${numeric.toLocaleString()}`
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Format numeric value with thousands separators
|
|
80
|
+
* @example formatNumber(1250000) => "1,250,000"
|
|
81
|
+
*/
|
|
82
|
+
export function formatNumber(
|
|
83
|
+
value: number | string | null | undefined,
|
|
84
|
+
locale = DEFAULT_LOCALE,
|
|
85
|
+
): string {
|
|
86
|
+
if (value === null || value === undefined || value === '') return '-'
|
|
87
|
+
const numeric = typeof value === 'string' ? Number(value) : value
|
|
88
|
+
if (Number.isNaN(numeric)) return '-'
|
|
89
|
+
|
|
90
|
+
return new Intl.NumberFormat(locale).format(numeric)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format large numbers to compact notation (e.g., 1.5K, 2.4M)
|
|
95
|
+
* @example formatCompactNumber(15400) => "15.4K"
|
|
96
|
+
*/
|
|
97
|
+
export function formatCompactNumber(
|
|
98
|
+
value: number | string | null | undefined,
|
|
99
|
+
locale = DEFAULT_LOCALE,
|
|
100
|
+
): string {
|
|
101
|
+
if (value === null || value === undefined || value === '') return '-'
|
|
102
|
+
const numeric = typeof value === 'string' ? Number(value) : value
|
|
103
|
+
if (Number.isNaN(numeric)) return '-'
|
|
104
|
+
|
|
105
|
+
return new Intl.NumberFormat(locale, {
|
|
106
|
+
notation: 'compact',
|
|
107
|
+
compactDisplay: 'short',
|
|
108
|
+
maximumFractionDigits: 1,
|
|
109
|
+
}).format(numeric)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format date string / Date object to localized readable string
|
|
114
|
+
* @example formatDate('2026-08-21T07:00:00Z') => "Aug 21, 2026"
|
|
115
|
+
* @example formatDate('2026-08-21', 'full') => "Friday, August 21, 2026"
|
|
116
|
+
*/
|
|
117
|
+
export function formatDate(
|
|
118
|
+
date: string | number | Date | null | undefined,
|
|
119
|
+
style: 'short' | 'medium' | 'long' | 'full' = 'medium',
|
|
120
|
+
locale = DEFAULT_LOCALE,
|
|
121
|
+
): string {
|
|
122
|
+
if (!date) return '-'
|
|
123
|
+
const parsedDate = date instanceof Date ? date : new Date(date)
|
|
124
|
+
if (Number.isNaN(parsedDate.getTime())) return '-'
|
|
125
|
+
|
|
126
|
+
const dateStyleMap: Record<'short' | 'medium' | 'long' | 'full', Intl.DateTimeFormatOptions['dateStyle']> = {
|
|
127
|
+
short: 'short',
|
|
128
|
+
medium: 'medium',
|
|
129
|
+
long: 'long',
|
|
130
|
+
full: 'full',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Intl.DateTimeFormat(locale, {
|
|
134
|
+
dateStyle: dateStyleMap[style],
|
|
135
|
+
}).format(parsedDate)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Helper to resolve hour12 boolean from '24h' | '12h' | 'auto'
|
|
140
|
+
*/
|
|
141
|
+
function resolveHour12(format: TimeFormat = DEFAULT_TIME_FORMAT): boolean | undefined {
|
|
142
|
+
if (format === '24h') return false
|
|
143
|
+
if (format === '12h') return true
|
|
144
|
+
return undefined // auto: browser locale determines 12h/24h
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Format date to include time string with timezone and 24h/12h clock support
|
|
149
|
+
* @example formatDateTime('2026-08-21T07:30:00Z') => "Aug 21, 2026, 14:30" (if 24h & WIB)
|
|
150
|
+
* @example formatDateTime(date, { format: '12h' }) => "Aug 21, 2026, 02:30 PM"
|
|
151
|
+
*/
|
|
152
|
+
export function formatDateTime(
|
|
153
|
+
date: string | number | Date | null | undefined,
|
|
154
|
+
options: DateTimeFormatOptions | string = {},
|
|
155
|
+
): string {
|
|
156
|
+
if (!date) return '-'
|
|
157
|
+
const parsedDate = date instanceof Date ? date : new Date(date)
|
|
158
|
+
if (Number.isNaN(parsedDate.getTime())) return '-'
|
|
159
|
+
|
|
160
|
+
const opts: DateTimeFormatOptions = typeof options === 'string' ? { locale: options } : options
|
|
161
|
+
const {
|
|
162
|
+
locale = DEFAULT_LOCALE,
|
|
163
|
+
timeZone = DEFAULT_TIMEZONE,
|
|
164
|
+
format = DEFAULT_TIME_FORMAT,
|
|
165
|
+
dateStyle = 'medium',
|
|
166
|
+
timeStyle = 'short',
|
|
167
|
+
} = opts
|
|
168
|
+
|
|
169
|
+
return new Intl.DateTimeFormat(locale, {
|
|
170
|
+
dateStyle,
|
|
171
|
+
timeStyle,
|
|
172
|
+
timeZone,
|
|
173
|
+
hour12: resolveHour12(format),
|
|
174
|
+
}).format(parsedDate)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format time only (without date) with 24h / 12h clock support
|
|
179
|
+
* @example formatTime('2026-08-21T07:30:00Z') => "14:30" (if 24h & WIB)
|
|
180
|
+
* @example formatTime('2026-08-21T07:30:00Z', { format: '12h' }) => "02:30 PM"
|
|
181
|
+
* @example formatTime('2026-08-21T07:30:00Z', { showSeconds: true }) => "14:30:00"
|
|
182
|
+
*/
|
|
183
|
+
export function formatTime(
|
|
184
|
+
date: string | number | Date | null | undefined,
|
|
185
|
+
options: TimeOnlyFormatOptions = {},
|
|
186
|
+
): string {
|
|
187
|
+
if (!date) return '-'
|
|
188
|
+
const parsedDate = date instanceof Date ? date : new Date(date)
|
|
189
|
+
if (Number.isNaN(parsedDate.getTime())) return '-'
|
|
190
|
+
|
|
191
|
+
const {
|
|
192
|
+
locale = DEFAULT_LOCALE,
|
|
193
|
+
timeZone = DEFAULT_TIMEZONE,
|
|
194
|
+
format = DEFAULT_TIME_FORMAT,
|
|
195
|
+
showSeconds = false,
|
|
196
|
+
} = options
|
|
197
|
+
|
|
198
|
+
return new Intl.DateTimeFormat(locale, {
|
|
199
|
+
hour: '2-digit',
|
|
200
|
+
minute: '2-digit',
|
|
201
|
+
...(showSeconds ? { second: '2-digit' } : {}),
|
|
202
|
+
timeZone,
|
|
203
|
+
hour12: resolveHour12(format),
|
|
204
|
+
}).format(parsedDate)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Format relative time (e.g. "5 minutes ago", "in 2 hours", "kemarin")
|
|
209
|
+
* Fully locale-aware including the "just now" / "baru saja" fallback phrase.
|
|
210
|
+
*/
|
|
211
|
+
export function formatRelativeTime(
|
|
212
|
+
date: string | number | Date | null | undefined,
|
|
213
|
+
locale = DEFAULT_LOCALE,
|
|
214
|
+
): string {
|
|
215
|
+
if (!date) return '-'
|
|
216
|
+
const parsedDate = date instanceof Date ? date : new Date(date)
|
|
217
|
+
if (Number.isNaN(parsedDate.getTime())) return '-'
|
|
218
|
+
|
|
219
|
+
const diffInSeconds = Math.round((parsedDate.getTime() - Date.now()) / 1000)
|
|
220
|
+
const absDiff = Math.abs(diffInSeconds)
|
|
221
|
+
|
|
222
|
+
// Locale-aware "just now" phrase map — add more locales as needed
|
|
223
|
+
const justNowPhrases: Record<string, string> = {
|
|
224
|
+
'id-ID': 'baru saja',
|
|
225
|
+
'id': 'baru saja',
|
|
226
|
+
'de-DE': 'gerade eben',
|
|
227
|
+
'fr-FR': "à l'instant",
|
|
228
|
+
'ja-JP': 'たった今',
|
|
229
|
+
'zh-CN': '刚才',
|
|
230
|
+
}
|
|
231
|
+
const justNow = justNowPhrases[locale] ?? 'just now'
|
|
232
|
+
|
|
233
|
+
if (absDiff < 45) return justNow
|
|
234
|
+
|
|
235
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
|
|
236
|
+
|
|
237
|
+
const intervals = [
|
|
238
|
+
{ unit: 'year', seconds: 31536000 },
|
|
239
|
+
{ unit: 'month', seconds: 2592000 },
|
|
240
|
+
{ unit: 'week', seconds: 604800 },
|
|
241
|
+
{ unit: 'day', seconds: 86400 },
|
|
242
|
+
{ unit: 'hour', seconds: 3600 },
|
|
243
|
+
{ unit: 'minute', seconds: 60 },
|
|
244
|
+
] as const
|
|
245
|
+
|
|
246
|
+
for (const { unit, seconds } of intervals) {
|
|
247
|
+
if (absDiff >= seconds) {
|
|
248
|
+
const delta = Math.round(diffInSeconds / seconds)
|
|
249
|
+
return rtf.format(delta, unit)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return justNow
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format raw bytes into human-readable data units (KB, MB, GB)
|
|
258
|
+
* Uses locale-aware decimal separator (e.g. "1,5 MB" for id-ID, "1.5 MB" for en-US)
|
|
259
|
+
* @example formatBytes(1048576) => "1 MB"
|
|
260
|
+
* @example formatBytes(1536, 1, 'id-ID') => "1,5 KB"
|
|
261
|
+
*/
|
|
262
|
+
export function formatBytes(
|
|
263
|
+
bytes: number | null | undefined,
|
|
264
|
+
decimals = 2,
|
|
265
|
+
locale = DEFAULT_LOCALE,
|
|
266
|
+
): string {
|
|
267
|
+
if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return '-'
|
|
268
|
+
if (bytes === 0) return '0 B'
|
|
269
|
+
|
|
270
|
+
const k = 1024
|
|
271
|
+
const dm = decimals < 0 ? 0 : decimals
|
|
272
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
|
273
|
+
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
|
|
274
|
+
const value = bytes / Math.pow(k, i)
|
|
275
|
+
|
|
276
|
+
const formatted = new Intl.NumberFormat(locale, {
|
|
277
|
+
minimumFractionDigits: 0,
|
|
278
|
+
maximumFractionDigits: dm,
|
|
279
|
+
}).format(value)
|
|
280
|
+
|
|
281
|
+
return `${formatted} ${sizes[i]}`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Format date with time and explicit timezone (important for Indonesia: WIB/WITA/WIT)
|
|
286
|
+
* @param timeZone IANA timezone string. Indonesia: 'Asia/Jakarta' (WIB), 'Asia/Makassar' (WITA), 'Asia/Jayapura' (WIT)
|
|
287
|
+
* @example formatDateTimeWithTz(date, 'Asia/Jakarta', '24h') => "21 Agt 2026, 14.30"
|
|
288
|
+
*/
|
|
289
|
+
export function formatDateTimeWithTz(
|
|
290
|
+
date: string | number | Date | null | undefined,
|
|
291
|
+
timeZone: string = DEFAULT_TIMEZONE,
|
|
292
|
+
format: TimeFormat = DEFAULT_TIME_FORMAT,
|
|
293
|
+
locale = DEFAULT_LOCALE,
|
|
294
|
+
): string {
|
|
295
|
+
if (!date) return '-'
|
|
296
|
+
const parsedDate = date instanceof Date ? date : new Date(date)
|
|
297
|
+
if (Number.isNaN(parsedDate.getTime())) return '-'
|
|
298
|
+
|
|
299
|
+
return new Intl.DateTimeFormat(locale, {
|
|
300
|
+
dateStyle: 'medium',
|
|
301
|
+
timeStyle: 'short',
|
|
302
|
+
timeZone,
|
|
303
|
+
hour12: resolveHour12(format),
|
|
304
|
+
}).format(parsedDate)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get 2-letter uppercase initials from a full name (safe for Avatar fallback)
|
|
309
|
+
* @example getInitials("Olivia Martin") => "OM"
|
|
310
|
+
* @example getInitials("John") => "JO"
|
|
311
|
+
*/
|
|
312
|
+
export function getInitials(name?: string | null): string {
|
|
313
|
+
if (!name || typeof name !== 'string') return 'NA'
|
|
314
|
+
const trimmed = name.trim()
|
|
315
|
+
if (!trimmed) return 'NA'
|
|
316
|
+
|
|
317
|
+
const parts = trimmed.split(/\s+/).filter(Boolean)
|
|
318
|
+
if (parts.length === 1) {
|
|
319
|
+
return parts[0].slice(0, 2).toUpperCase()
|
|
320
|
+
}
|
|
321
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Truncate long strings with ellipsis
|
|
326
|
+
* @example truncate("This is a very long text", 10) => "This is a..."
|
|
327
|
+
*/
|
|
328
|
+
export function truncate(text: string | null | undefined, length = 30): string {
|
|
329
|
+
if (!text) return ''
|
|
330
|
+
if (text.length <= length) return text
|
|
331
|
+
return `${text.slice(0, length)}...`
|
|
332
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface LoadingBarState {
|
|
4
|
+
progress: number
|
|
5
|
+
status: 'idle' | 'loading' | 'success' | 'error'
|
|
6
|
+
visible: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const progress = ref(0)
|
|
10
|
+
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
11
|
+
const visible = ref(false)
|
|
12
|
+
|
|
13
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
14
|
+
let activeRequests = 0
|
|
15
|
+
|
|
16
|
+
function clearTimer() {
|
|
17
|
+
if (timer) {
|
|
18
|
+
clearInterval(timer)
|
|
19
|
+
timer = null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Global Top Loading Bar Controller
|
|
25
|
+
* Supports multi-request counting and automatic micro-increments.
|
|
26
|
+
*/
|
|
27
|
+
export const loadingBar = {
|
|
28
|
+
progress,
|
|
29
|
+
status,
|
|
30
|
+
visible,
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start or increment progress
|
|
34
|
+
*/
|
|
35
|
+
start() {
|
|
36
|
+
activeRequests++
|
|
37
|
+
if (activeRequests > 1) return
|
|
38
|
+
|
|
39
|
+
clearTimer()
|
|
40
|
+
status.value = 'loading'
|
|
41
|
+
visible.value = true
|
|
42
|
+
progress.value = 10
|
|
43
|
+
|
|
44
|
+
timer = setInterval(() => {
|
|
45
|
+
if (progress.value < 85) {
|
|
46
|
+
// Slow down as progress approaches 85%
|
|
47
|
+
const inc = Math.max(1, (90 - progress.value) / 10)
|
|
48
|
+
progress.value = Math.min(85, progress.value + inc)
|
|
49
|
+
}
|
|
50
|
+
}, 150)
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Finish loading with a smooth transition to 100%
|
|
55
|
+
*/
|
|
56
|
+
finish() {
|
|
57
|
+
activeRequests = Math.max(0, activeRequests - 1)
|
|
58
|
+
if (activeRequests > 0) return
|
|
59
|
+
|
|
60
|
+
clearTimer()
|
|
61
|
+
progress.value = 100
|
|
62
|
+
status.value = 'success'
|
|
63
|
+
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
visible.value = false
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
progress.value = 0
|
|
68
|
+
status.value = 'idle'
|
|
69
|
+
}, 200)
|
|
70
|
+
}, 300)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fail loading bar with an error state (red bar)
|
|
75
|
+
*/
|
|
76
|
+
fail() {
|
|
77
|
+
activeRequests = Math.max(0, activeRequests - 1)
|
|
78
|
+
if (activeRequests > 0) return
|
|
79
|
+
|
|
80
|
+
clearTimer()
|
|
81
|
+
progress.value = 100
|
|
82
|
+
status.value = 'error'
|
|
83
|
+
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
visible.value = false
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
progress.value = 0
|
|
88
|
+
status.value = 'idle'
|
|
89
|
+
}, 200)
|
|
90
|
+
}, 400)
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Force reset loading bar
|
|
95
|
+
*/
|
|
96
|
+
reset() {
|
|
97
|
+
activeRequests = 0
|
|
98
|
+
clearTimer()
|
|
99
|
+
visible.value = false
|
|
100
|
+
progress.value = 0
|
|
101
|
+
status.value = 'idle'
|
|
102
|
+
},
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { cn } from './utils'
|
|
3
|
+
|
|
4
|
+
describe('cn utility', () => {
|
|
5
|
+
it('merges class names correctly', () => {
|
|
6
|
+
const result = cn('px-2 py-1', 'bg-blue-500')
|
|
7
|
+
expect(result).toBe('px-2 py-1 bg-blue-500')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('handles conditional class names', () => {
|
|
11
|
+
const isActive = true
|
|
12
|
+
const isDisabled = false
|
|
13
|
+
const result = cn('btn', isActive && 'btn-active', isDisabled && 'btn-disabled')
|
|
14
|
+
expect(result).toBe('btn btn-active')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('resolves tailwind class conflicts using tailwind-merge', () => {
|
|
18
|
+
const result = cn('p-4', 'p-2', 'text-red-500', 'text-blue-500')
|
|
19
|
+
expect(result).toBe('p-2 text-blue-500')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('handles array and object inputs', () => {
|
|
23
|
+
const result = cn(['base-class', 'extra-class'], { 'is-open': true, 'is-hidden': false })
|
|
24
|
+
expect(result).toBe('base-class extra-class is-open')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
const loginSchema = z.object({
|
|
5
|
+
email: z.string().min(1, 'Email is required').email('Invalid email address'),
|
|
6
|
+
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const userFormSchema = z.object({
|
|
10
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
11
|
+
email: z.string().email('Invalid email address'),
|
|
12
|
+
role: z.enum(['Admin', 'Developer', 'Editor', 'Viewer']),
|
|
13
|
+
status: z.enum(['Active', 'Pending', 'Suspended']),
|
|
14
|
+
twoFactor: z.boolean().default(false),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('Form Validation Schemas (Zod)', () => {
|
|
18
|
+
describe('loginSchema', () => {
|
|
19
|
+
it('validates correct email and password', () => {
|
|
20
|
+
const validData = {
|
|
21
|
+
email: 'admin@nala.dev',
|
|
22
|
+
password: 'password123',
|
|
23
|
+
}
|
|
24
|
+
const result = loginSchema.safeParse(validData)
|
|
25
|
+
expect(result.success).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects invalid email formats', () => {
|
|
29
|
+
const invalidData = {
|
|
30
|
+
email: 'not-an-email',
|
|
31
|
+
password: 'password123',
|
|
32
|
+
}
|
|
33
|
+
const result = loginSchema.safeParse(invalidData)
|
|
34
|
+
expect(result.success).toBe(false)
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
expect(result.error.issues[0].message).toBe('Invalid email address')
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('rejects short passwords', () => {
|
|
41
|
+
const shortPass = {
|
|
42
|
+
email: 'admin@nala.dev',
|
|
43
|
+
password: '123',
|
|
44
|
+
}
|
|
45
|
+
const result = loginSchema.safeParse(shortPass)
|
|
46
|
+
expect(result.success).toBe(false)
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
expect(result.error.issues[0].message).toBe('Password must be at least 6 characters')
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('userFormSchema', () => {
|
|
54
|
+
it('accepts valid user creation payload', () => {
|
|
55
|
+
const validUser = {
|
|
56
|
+
name: 'Olivia Martin',
|
|
57
|
+
email: 'olivia@supabase.io',
|
|
58
|
+
role: 'Admin',
|
|
59
|
+
status: 'Active',
|
|
60
|
+
twoFactor: true,
|
|
61
|
+
}
|
|
62
|
+
const result = userFormSchema.safeParse(validUser)
|
|
63
|
+
expect(result.success).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('rejects invalid role selection', () => {
|
|
67
|
+
const invalidUser = {
|
|
68
|
+
name: 'Olivia Martin',
|
|
69
|
+
email: 'olivia@supabase.io',
|
|
70
|
+
role: 'SuperMaster',
|
|
71
|
+
status: 'Active',
|
|
72
|
+
twoFactor: true,
|
|
73
|
+
}
|
|
74
|
+
const result = userFormSchema.safeParse(invalidUser)
|
|
75
|
+
expect(result.success).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
import 'vue-sonner/style.css'
|
|
4
|
+
import './style.css'
|
|
5
|
+
import App from './App.vue'
|
|
6
|
+
import router from './router'
|
|
7
|
+
|
|
8
|
+
const app = createApp(App)
|
|
9
|
+
|
|
10
|
+
app.use(createPinia())
|
|
11
|
+
app.use(router)
|
|
12
|
+
|
|
13
|
+
app.mount('#app')
|