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.
Files changed (316) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/index.js +227 -0
  4. package/package.json +48 -0
  5. package/template/.env.example +45 -0
  6. package/template/_gitignore +33 -0
  7. package/template/components.json +24 -0
  8. package/template/index.html +30 -0
  9. package/template/package.json +51 -0
  10. package/template/public/favicon.svg +15 -0
  11. package/template/public/icons.svg +24 -0
  12. package/template/public/img/avatar.webp +0 -0
  13. package/template/public/screenshot.png +0 -0
  14. package/template/src/App.vue +15 -0
  15. package/template/src/auto-imports.d.ts +691 -0
  16. package/template/src/components/AppLogo.vue +159 -0
  17. package/template/src/components/EmptyState.vue +60 -0
  18. package/template/src/components/PageHeader.vue +60 -0
  19. package/template/src/components/layout/AppNavbar.vue +159 -0
  20. package/template/src/components/layout/AppSidebar.vue +74 -0
  21. package/template/src/components/layout/CommandSearchDialog.vue +120 -0
  22. package/template/src/components/ui/accordion/Accordion.vue +15 -0
  23. package/template/src/components/ui/accordion/AccordionContent.vue +20 -0
  24. package/template/src/components/ui/accordion/AccordionItem.vue +20 -0
  25. package/template/src/components/ui/accordion/AccordionTrigger.vue +34 -0
  26. package/template/src/components/ui/accordion/index.ts +4 -0
  27. package/template/src/components/ui/alert/Alert.vue +21 -0
  28. package/template/src/components/ui/alert/AlertDescription.vue +17 -0
  29. package/template/src/components/ui/alert/AlertTitle.vue +17 -0
  30. package/template/src/components/ui/alert/index.ts +30 -0
  31. package/template/src/components/ui/alert-dialog/AlertDialog.vue +15 -0
  32. package/template/src/components/ui/alert-dialog/AlertDialogAction.vue +18 -0
  33. package/template/src/components/ui/alert-dialog/AlertDialogCancel.vue +25 -0
  34. package/template/src/components/ui/alert-dialog/AlertDialogContent.vue +44 -0
  35. package/template/src/components/ui/alert-dialog/AlertDialogDescription.vue +23 -0
  36. package/template/src/components/ui/alert-dialog/AlertDialogFooter.vue +22 -0
  37. package/template/src/components/ui/alert-dialog/AlertDialogHeader.vue +17 -0
  38. package/template/src/components/ui/alert-dialog/AlertDialogTitle.vue +21 -0
  39. package/template/src/components/ui/alert-dialog/AlertDialogTrigger.vue +12 -0
  40. package/template/src/components/ui/alert-dialog/index.ts +9 -0
  41. package/template/src/components/ui/avatar/Avatar.vue +50 -0
  42. package/template/src/components/ui/avatar/AvatarFallback.vue +21 -0
  43. package/template/src/components/ui/avatar/AvatarGroup.vue +69 -0
  44. package/template/src/components/ui/avatar/AvatarImage.vue +16 -0
  45. package/template/src/components/ui/avatar/index.ts +4 -0
  46. package/template/src/components/ui/badge/Badge.vue +40 -0
  47. package/template/src/components/ui/badge/index.ts +45 -0
  48. package/template/src/components/ui/breadcrumb/Breadcrumb.vue +17 -0
  49. package/template/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +23 -0
  50. package/template/src/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
  51. package/template/src/components/ui/breadcrumb/BreadcrumbLink.vue +21 -0
  52. package/template/src/components/ui/breadcrumb/BreadcrumbList.vue +17 -0
  53. package/template/src/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
  54. package/template/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +22 -0
  55. package/template/src/components/ui/breadcrumb/index.ts +7 -0
  56. package/template/src/components/ui/button/Button.spec.ts +46 -0
  57. package/template/src/components/ui/button/Button.vue +31 -0
  58. package/template/src/components/ui/button/index.ts +41 -0
  59. package/template/src/components/ui/calendar/Calendar.vue +106 -0
  60. package/template/src/components/ui/calendar/index.ts +1 -0
  61. package/template/src/components/ui/card/Card.vue +29 -0
  62. package/template/src/components/ui/card/CardAction.vue +17 -0
  63. package/template/src/components/ui/card/CardContent.vue +17 -0
  64. package/template/src/components/ui/card/CardDescription.vue +17 -0
  65. package/template/src/components/ui/card/CardFooter.vue +17 -0
  66. package/template/src/components/ui/card/CardHeader.vue +29 -0
  67. package/template/src/components/ui/card/CardTitle.vue +17 -0
  68. package/template/src/components/ui/card/index.ts +7 -0
  69. package/template/src/components/ui/chart/AreaChart.vue +143 -0
  70. package/template/src/components/ui/chart/BarChart.vue +142 -0
  71. package/template/src/components/ui/chart/DonutChart.vue +75 -0
  72. package/template/src/components/ui/chart/LineChart.vue +132 -0
  73. package/template/src/components/ui/chart/index.ts +4 -0
  74. package/template/src/components/ui/checkbox/Checkbox.vue +57 -0
  75. package/template/src/components/ui/checkbox/index.ts +1 -0
  76. package/template/src/components/ui/collapsible/Collapsible.vue +15 -0
  77. package/template/src/components/ui/collapsible/CollapsibleContent.vue +18 -0
  78. package/template/src/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  79. package/template/src/components/ui/collapsible/index.ts +3 -0
  80. package/template/src/components/ui/command/Command.vue +88 -0
  81. package/template/src/components/ui/command/CommandDialog.vue +31 -0
  82. package/template/src/components/ui/command/CommandEmpty.vue +27 -0
  83. package/template/src/components/ui/command/CommandGroup.vue +45 -0
  84. package/template/src/components/ui/command/CommandInput.vue +39 -0
  85. package/template/src/components/ui/command/CommandItem.vue +76 -0
  86. package/template/src/components/ui/command/CommandList.vue +25 -0
  87. package/template/src/components/ui/command/CommandSeparator.vue +21 -0
  88. package/template/src/components/ui/command/CommandShortcut.vue +17 -0
  89. package/template/src/components/ui/command/index.ts +25 -0
  90. package/template/src/components/ui/context-menu/ContextMenu.vue +15 -0
  91. package/template/src/components/ui/context-menu/ContextMenuCheckboxItem.vue +36 -0
  92. package/template/src/components/ui/context-menu/ContextMenuContent.vue +33 -0
  93. package/template/src/components/ui/context-menu/ContextMenuGroup.vue +12 -0
  94. package/template/src/components/ui/context-menu/ContextMenuItem.vue +36 -0
  95. package/template/src/components/ui/context-menu/ContextMenuLabel.vue +25 -0
  96. package/template/src/components/ui/context-menu/ContextMenuRadioGroup.vue +18 -0
  97. package/template/src/components/ui/context-menu/ContextMenuRadioItem.vue +36 -0
  98. package/template/src/components/ui/context-menu/ContextMenuSeparator.vue +18 -0
  99. package/template/src/components/ui/context-menu/ContextMenuShortcut.vue +17 -0
  100. package/template/src/components/ui/context-menu/ContextMenuSub.vue +15 -0
  101. package/template/src/components/ui/context-menu/ContextMenuSubContent.vue +36 -0
  102. package/template/src/components/ui/context-menu/ContextMenuSubTrigger.vue +32 -0
  103. package/template/src/components/ui/context-menu/ContextMenuTrigger.vue +13 -0
  104. package/template/src/components/ui/context-menu/index.ts +14 -0
  105. package/template/src/components/ui/date-picker/DatePicker.vue +233 -0
  106. package/template/src/components/ui/date-picker/index.ts +1 -0
  107. package/template/src/components/ui/dialog/Dialog.vue +19 -0
  108. package/template/src/components/ui/dialog/DialogClose.vue +15 -0
  109. package/template/src/components/ui/dialog/DialogContent.vue +53 -0
  110. package/template/src/components/ui/dialog/DialogDescription.vue +23 -0
  111. package/template/src/components/ui/dialog/DialogFooter.vue +27 -0
  112. package/template/src/components/ui/dialog/DialogHeader.vue +17 -0
  113. package/template/src/components/ui/dialog/DialogOverlay.vue +21 -0
  114. package/template/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  115. package/template/src/components/ui/dialog/DialogTitle.vue +23 -0
  116. package/template/src/components/ui/dialog/DialogTrigger.vue +15 -0
  117. package/template/src/components/ui/dialog/index.ts +10 -0
  118. package/template/src/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  119. package/template/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -0
  120. package/template/src/components/ui/dropdown-menu/DropdownMenuContent.vue +41 -0
  121. package/template/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -0
  122. package/template/src/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -0
  123. package/template/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
  124. package/template/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  125. package/template/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  126. package/template/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  127. package/template/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  128. package/template/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
  129. package/template/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -0
  130. package/template/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  131. package/template/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  132. package/template/src/components/ui/dropdown-menu/index.ts +16 -0
  133. package/template/src/components/ui/form/FormControl.vue +17 -0
  134. package/template/src/components/ui/form/FormDescription.vue +21 -0
  135. package/template/src/components/ui/form/FormItem.vue +23 -0
  136. package/template/src/components/ui/form/FormLabel.vue +25 -0
  137. package/template/src/components/ui/form/FormMessage.vue +23 -0
  138. package/template/src/components/ui/form/index.ts +7 -0
  139. package/template/src/components/ui/form/injectionKeys.ts +4 -0
  140. package/template/src/components/ui/form/useFormField.ts +30 -0
  141. package/template/src/components/ui/hover-card/HoverCard.vue +19 -0
  142. package/template/src/components/ui/hover-card/HoverCardContent.vue +43 -0
  143. package/template/src/components/ui/hover-card/HoverCardTrigger.vue +15 -0
  144. package/template/src/components/ui/hover-card/index.ts +3 -0
  145. package/template/src/components/ui/index.ts +47 -0
  146. package/template/src/components/ui/input/Input.vue +33 -0
  147. package/template/src/components/ui/input/InputAddon.vue +29 -0
  148. package/template/src/components/ui/input/InputGroup.vue +17 -0
  149. package/template/src/components/ui/input/InputIcon.vue +29 -0
  150. package/template/src/components/ui/input/index.ts +4 -0
  151. package/template/src/components/ui/kbd/Kbd.vue +30 -0
  152. package/template/src/components/ui/kbd/index.ts +1 -0
  153. package/template/src/components/ui/label/Label.vue +26 -0
  154. package/template/src/components/ui/label/index.ts +1 -0
  155. package/template/src/components/ui/loading-bar/LoadingBar.vue +41 -0
  156. package/template/src/components/ui/loading-bar/index.ts +2 -0
  157. package/template/src/components/ui/pagination/Pagination.vue +26 -0
  158. package/template/src/components/ui/pagination/PaginationContent.vue +22 -0
  159. package/template/src/components/ui/pagination/PaginationEllipsis.vue +25 -0
  160. package/template/src/components/ui/pagination/PaginationFirst.vue +33 -0
  161. package/template/src/components/ui/pagination/PaginationItem.vue +34 -0
  162. package/template/src/components/ui/pagination/PaginationLast.vue +33 -0
  163. package/template/src/components/ui/pagination/PaginationNext.vue +33 -0
  164. package/template/src/components/ui/pagination/PaginationPrevious.vue +33 -0
  165. package/template/src/components/ui/pagination/index.ts +8 -0
  166. package/template/src/components/ui/pin-input/PinInput.vue +28 -0
  167. package/template/src/components/ui/pin-input/PinInputGroup.vue +18 -0
  168. package/template/src/components/ui/pin-input/PinInputInput.vue +24 -0
  169. package/template/src/components/ui/pin-input/PinInputSeparator.vue +21 -0
  170. package/template/src/components/ui/pin-input/index.ts +4 -0
  171. package/template/src/components/ui/popover/Popover.vue +19 -0
  172. package/template/src/components/ui/popover/PopoverAnchor.vue +15 -0
  173. package/template/src/components/ui/popover/PopoverContent.vue +45 -0
  174. package/template/src/components/ui/popover/PopoverTrigger.vue +15 -0
  175. package/template/src/components/ui/popover/index.ts +4 -0
  176. package/template/src/components/ui/progress/Progress.vue +35 -0
  177. package/template/src/components/ui/progress/index.ts +1 -0
  178. package/template/src/components/ui/radio-group/RadioGroup.vue +23 -0
  179. package/template/src/components/ui/radio-group/RadioGroupItem.vue +30 -0
  180. package/template/src/components/ui/radio-group/index.ts +2 -0
  181. package/template/src/components/ui/scroll-area/ScrollArea.vue +40 -0
  182. package/template/src/components/ui/scroll-area/ScrollBar.vue +44 -0
  183. package/template/src/components/ui/scroll-area/index.ts +2 -0
  184. package/template/src/components/ui/select/Select.vue +19 -0
  185. package/template/src/components/ui/select/SelectContent.vue +51 -0
  186. package/template/src/components/ui/select/SelectGroup.vue +15 -0
  187. package/template/src/components/ui/select/SelectItem.vue +44 -0
  188. package/template/src/components/ui/select/SelectItemText.vue +15 -0
  189. package/template/src/components/ui/select/SelectLabel.vue +17 -0
  190. package/template/src/components/ui/select/SelectScrollDownButton.vue +26 -0
  191. package/template/src/components/ui/select/SelectScrollUpButton.vue +26 -0
  192. package/template/src/components/ui/select/SelectSeparator.vue +19 -0
  193. package/template/src/components/ui/select/SelectTrigger.vue +33 -0
  194. package/template/src/components/ui/select/SelectValue.vue +15 -0
  195. package/template/src/components/ui/select/index.ts +11 -0
  196. package/template/src/components/ui/separator/Separator.vue +29 -0
  197. package/template/src/components/ui/separator/index.ts +1 -0
  198. package/template/src/components/ui/sheet/Sheet.vue +19 -0
  199. package/template/src/components/ui/sheet/SheetClose.vue +15 -0
  200. package/template/src/components/ui/sheet/SheetContent.vue +62 -0
  201. package/template/src/components/ui/sheet/SheetDescription.vue +21 -0
  202. package/template/src/components/ui/sheet/SheetFooter.vue +16 -0
  203. package/template/src/components/ui/sheet/SheetHeader.vue +15 -0
  204. package/template/src/components/ui/sheet/SheetOverlay.vue +21 -0
  205. package/template/src/components/ui/sheet/SheetTitle.vue +21 -0
  206. package/template/src/components/ui/sheet/SheetTrigger.vue +15 -0
  207. package/template/src/components/ui/sheet/index.ts +8 -0
  208. package/template/src/components/ui/sidebar/Sidebar.vue +113 -0
  209. package/template/src/components/ui/sidebar/SidebarContent.vue +18 -0
  210. package/template/src/components/ui/sidebar/SidebarControl.vue +53 -0
  211. package/template/src/components/ui/sidebar/SidebarFooter.vue +18 -0
  212. package/template/src/components/ui/sidebar/SidebarGroup.vue +18 -0
  213. package/template/src/components/ui/sidebar/SidebarGroupAction.vue +27 -0
  214. package/template/src/components/ui/sidebar/SidebarGroupContent.vue +18 -0
  215. package/template/src/components/ui/sidebar/SidebarGroupLabel.vue +25 -0
  216. package/template/src/components/ui/sidebar/SidebarHeader.vue +18 -0
  217. package/template/src/components/ui/sidebar/SidebarInput.vue +22 -0
  218. package/template/src/components/ui/sidebar/SidebarInset.vue +21 -0
  219. package/template/src/components/ui/sidebar/SidebarMenu.vue +18 -0
  220. package/template/src/components/ui/sidebar/SidebarMenuAction.vue +35 -0
  221. package/template/src/components/ui/sidebar/SidebarMenuBadge.vue +26 -0
  222. package/template/src/components/ui/sidebar/SidebarMenuButton.vue +48 -0
  223. package/template/src/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
  224. package/template/src/components/ui/sidebar/SidebarMenuItem.vue +18 -0
  225. package/template/src/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
  226. package/template/src/components/ui/sidebar/SidebarMenuSub.vue +22 -0
  227. package/template/src/components/ui/sidebar/SidebarMenuSubButton.vue +36 -0
  228. package/template/src/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  229. package/template/src/components/ui/sidebar/SidebarProvider.vue +136 -0
  230. package/template/src/components/ui/sidebar/SidebarRail.vue +33 -0
  231. package/template/src/components/ui/sidebar/SidebarSeparator.vue +19 -0
  232. package/template/src/components/ui/sidebar/SidebarTrigger.vue +27 -0
  233. package/template/src/components/ui/sidebar/index.ts +61 -0
  234. package/template/src/components/ui/sidebar/utils.ts +27 -0
  235. package/template/src/components/ui/skeleton/Skeleton.vue +17 -0
  236. package/template/src/components/ui/skeleton/index.ts +1 -0
  237. package/template/src/components/ui/slider/Slider.vue +42 -0
  238. package/template/src/components/ui/slider/index.ts +1 -0
  239. package/template/src/components/ui/sonner/Sonner.vue +42 -0
  240. package/template/src/components/ui/sonner/index.ts +2 -0
  241. package/template/src/components/ui/stepper/Stepper.vue +102 -0
  242. package/template/src/components/ui/stepper/index.ts +1 -0
  243. package/template/src/components/ui/switch/Switch.vue +38 -0
  244. package/template/src/components/ui/switch/index.ts +1 -0
  245. package/template/src/components/ui/table/Table.vue +16 -0
  246. package/template/src/components/ui/table/TableBody.vue +17 -0
  247. package/template/src/components/ui/table/TableCaption.vue +17 -0
  248. package/template/src/components/ui/table/TableCell.vue +22 -0
  249. package/template/src/components/ui/table/TableEmpty.vue +34 -0
  250. package/template/src/components/ui/table/TableFooter.vue +17 -0
  251. package/template/src/components/ui/table/TableHead.vue +17 -0
  252. package/template/src/components/ui/table/TableHeader.vue +17 -0
  253. package/template/src/components/ui/table/TableRow.vue +17 -0
  254. package/template/src/components/ui/table/index.ts +9 -0
  255. package/template/src/components/ui/table/utils.ts +10 -0
  256. package/template/src/components/ui/tabs/Tabs.vue +24 -0
  257. package/template/src/components/ui/tabs/TabsContent.vue +21 -0
  258. package/template/src/components/ui/tabs/TabsList.vue +24 -0
  259. package/template/src/components/ui/tabs/TabsTrigger.vue +26 -0
  260. package/template/src/components/ui/tabs/index.ts +4 -0
  261. package/template/src/components/ui/textarea/Textarea.vue +28 -0
  262. package/template/src/components/ui/textarea/index.ts +1 -0
  263. package/template/src/components/ui/timeline/Timeline.vue +73 -0
  264. package/template/src/components/ui/timeline/index.ts +1 -0
  265. package/template/src/components/ui/toggle/Toggle.vue +35 -0
  266. package/template/src/components/ui/toggle/index.ts +28 -0
  267. package/template/src/components/ui/toggle-group/ToggleGroup.vue +41 -0
  268. package/template/src/components/ui/toggle-group/ToggleGroupItem.vue +43 -0
  269. package/template/src/components/ui/toggle-group/index.ts +12 -0
  270. package/template/src/components/ui/tooltip/Tooltip.vue +19 -0
  271. package/template/src/components/ui/tooltip/TooltipContent.vue +34 -0
  272. package/template/src/components/ui/tooltip/TooltipProvider.vue +14 -0
  273. package/template/src/components/ui/tooltip/TooltipTrigger.vue +15 -0
  274. package/template/src/components/ui/tooltip/index.ts +4 -0
  275. package/template/src/components.d.ts +228 -0
  276. package/template/src/composables/useFormatter.ts +105 -0
  277. package/template/src/composables/useThemeConfig.ts +252 -0
  278. package/template/src/layouts/AdminLayout.vue +55 -0
  279. package/template/src/layouts/AuthLayout.vue +42 -0
  280. package/template/src/lib/axios.spec.ts +46 -0
  281. package/template/src/lib/axios.ts +243 -0
  282. package/template/src/lib/formatters.spec.ts +137 -0
  283. package/template/src/lib/formatters.ts +332 -0
  284. package/template/src/lib/loading-bar.ts +103 -0
  285. package/template/src/lib/utils.spec.ts +26 -0
  286. package/template/src/lib/utils.ts +7 -0
  287. package/template/src/lib/validation.spec.ts +78 -0
  288. package/template/src/main.ts +13 -0
  289. package/template/src/router/index.ts +147 -0
  290. package/template/src/services/auth.service.spec.ts +93 -0
  291. package/template/src/services/auth.service.ts +243 -0
  292. package/template/src/services/index.ts +3 -0
  293. package/template/src/services/types.ts +28 -0
  294. package/template/src/services/user.service.spec.ts +91 -0
  295. package/template/src/services/user.service.ts +283 -0
  296. package/template/src/stores/auth.spec.ts +52 -0
  297. package/template/src/stores/auth.ts +130 -0
  298. package/template/src/style.css +587 -0
  299. package/template/src/views/_starter/BlankView.vue +47 -0
  300. package/template/src/views/auth/ConfirmEmailView.vue +209 -0
  301. package/template/src/views/auth/ForgotPasswordView.vue +151 -0
  302. package/template/src/views/auth/LockScreenView.vue +175 -0
  303. package/template/src/views/auth/LoginView.vue +54 -0
  304. package/template/src/views/auth/RegisterView.vue +265 -0
  305. package/template/src/views/auth/ResetPasswordView.vue +260 -0
  306. package/template/src/views/auth/VerifyOtpView.vue +261 -0
  307. package/template/src/views/dashboard/IndexView.vue +159 -0
  308. package/template/src/views/errors/ComingSoonView.vue +241 -0
  309. package/template/src/views/errors/MaintenanceView.vue +252 -0
  310. package/template/src/views/errors/NotFoundView.vue +81 -0
  311. package/template/src/views/errors/ServerErrorView.vue +101 -0
  312. package/template/src/views/errors/UnauthorizedView.vue +64 -0
  313. package/template/tsconfig.app.json +27 -0
  314. package/template/tsconfig.json +19 -0
  315. package/template/tsconfig.node.json +23 -0
  316. package/template/vite.config.ts +46 -0
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import { RouterView } from 'vue-router'
3
+ import { usePreferredDark } from '@vueuse/core'
4
+ import { watchEffect, onUnmounted } from 'vue'
5
+
6
+ // Strictly follow OS system preference (dark / light) on all auth pages
7
+ const prefersDark = usePreferredDark()
8
+ const previousTheme = localStorage.getItem('vueuse-color-scheme')
9
+
10
+ watchEffect(() => {
11
+ if (prefersDark.value) {
12
+ document.documentElement.classList.add('dark')
13
+ } else {
14
+ document.documentElement.classList.remove('dark')
15
+ }
16
+ })
17
+
18
+ // Restore stored admin theme preference when navigating away from auth layout
19
+ onUnmounted(() => {
20
+ if (previousTheme === 'dark') {
21
+ document.documentElement.classList.add('dark')
22
+ } else if (previousTheme === 'light') {
23
+ document.documentElement.classList.remove('dark')
24
+ } else {
25
+ if (prefersDark.value) {
26
+ document.documentElement.classList.add('dark')
27
+ } else {
28
+ document.documentElement.classList.remove('dark')
29
+ }
30
+ }
31
+ })
32
+ </script>
33
+
34
+ <template>
35
+ <div class="flex min-h-dvh w-full flex-col items-center justify-start pt-8 pb-12 px-4 sm:justify-center sm:py-8 sm:px-6 bg-muted/40 transition-colors duration-200 overflow-y-auto">
36
+ <RouterView v-slot="{ Component }">
37
+ <Transition name="page" mode="out-in">
38
+ <component :is="Component" />
39
+ </Transition>
40
+ </RouterView>
41
+ </div>
42
+ </template>
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import { api, tokenStorage } from './axios'
3
+
4
+ vi.mock('@/components/ui/sonner', () => ({
5
+ toast: {
6
+ error: vi.fn(),
7
+ success: vi.fn(),
8
+ info: vi.fn(),
9
+ },
10
+ }))
11
+
12
+ vi.mock('@/lib/loading-bar', () => ({
13
+ loadingBar: {
14
+ start: vi.fn(),
15
+ finish: vi.fn(),
16
+ fail: vi.fn(),
17
+ },
18
+ }))
19
+
20
+ describe('Axios Client & Token Storage', () => {
21
+ beforeEach(() => {
22
+ localStorage.clear()
23
+ vi.clearAllMocks()
24
+ })
25
+
26
+ afterEach(() => {
27
+ localStorage.clear()
28
+ })
29
+
30
+ it('manages accessToken and refreshToken in storage', () => {
31
+ tokenStorage.setAccessToken('access_token_abc')
32
+ tokenStorage.setRefreshToken('refresh_token_xyz')
33
+
34
+ expect(tokenStorage.getAccessToken()).toBe('access_token_abc')
35
+ expect(tokenStorage.getRefreshToken()).toBe('refresh_token_xyz')
36
+
37
+ tokenStorage.clearTokens()
38
+ expect(tokenStorage.getAccessToken()).toBeNull()
39
+ expect(tokenStorage.getRefreshToken()).toBeNull()
40
+ })
41
+
42
+ it('has base configuration with JSON content type', () => {
43
+ expect(api.defaults.headers['Content-Type']).toBe('application/json')
44
+ expect(api.defaults.timeout).toBe(15000)
45
+ })
46
+ })
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Enterprise Axios Client & Interceptor Layer
3
+ * -------------------------------------------
4
+ * Features:
5
+ * - Base URL & timeout configuration
6
+ * - Automatic Bearer token injection
7
+ * - Silent Token Refresh queue with 401 auto-retry
8
+ * - Global Error Toasts via vue-sonner (toggleable with `skipToast`)
9
+ * - Global Route / API Loading Bar integration
10
+ * - TypeScript Generics support for typed responses
11
+ */
12
+
13
+ import axios, {
14
+ type AxiosError,
15
+ type AxiosInstance,
16
+ type AxiosRequestConfig,
17
+ type AxiosResponse,
18
+ type InternalAxiosRequestConfig,
19
+ } from 'axios'
20
+ import { toast } from '@/components/ui/sonner'
21
+ import { loadingBar } from '@/lib/loading-bar'
22
+
23
+ // ─── Custom Request Config Type ───────────────────────────────────────────────
24
+ export interface CustomRequestConfig extends AxiosRequestConfig {
25
+ /** Skip auto toast notification on error */
26
+ skipToast?: boolean
27
+ /** Skip Bearer authorization header injection */
28
+ skipAuth?: boolean
29
+ /** Skip global loading bar progress */
30
+ skipLoading?: boolean
31
+ /** Internal retry flag for 401 token refresh */
32
+ _retry?: boolean
33
+ }
34
+
35
+ interface CustomInternalRequestConfig extends InternalAxiosRequestConfig {
36
+ skipToast?: boolean
37
+ skipAuth?: boolean
38
+ skipLoading?: boolean
39
+ _retry?: boolean
40
+ }
41
+
42
+ // ─── Storage Keys & Helpers ───────────────────────────────────────────────────
43
+ export const TOKEN_KEY = 'auth_token'
44
+ export const REFRESH_TOKEN_KEY = 'auth_refresh_token'
45
+
46
+ export const tokenStorage = {
47
+ getAccessToken(): string | null {
48
+ return localStorage.getItem(TOKEN_KEY)
49
+ },
50
+ setAccessToken(token: string) {
51
+ localStorage.setItem(TOKEN_KEY, token)
52
+ },
53
+ getRefreshToken(): string | null {
54
+ return localStorage.getItem(REFRESH_TOKEN_KEY)
55
+ },
56
+ setRefreshToken(token: string) {
57
+ localStorage.setItem(REFRESH_TOKEN_KEY, token)
58
+ },
59
+ clearTokens() {
60
+ localStorage.removeItem(TOKEN_KEY)
61
+ localStorage.removeItem(REFRESH_TOKEN_KEY)
62
+ },
63
+ }
64
+
65
+ // ─── Create Instance ──────────────────────────────────────────────────────────
66
+ export const api: AxiosInstance = axios.create({
67
+ baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000',
68
+ timeout: 15_000,
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ Accept: 'application/json',
72
+ },
73
+ })
74
+
75
+ // ─── Token Refresh Queue Manager ──────────────────────────────────────────────
76
+ let isRefreshing = false
77
+ let failedQueue: Array<{
78
+ resolve: (value?: unknown) => void
79
+ reject: (reason?: unknown) => void
80
+ }> = []
81
+
82
+ const processQueue = (error: AxiosError | null, token: string | null = null) => {
83
+ failedQueue.forEach((promise) => {
84
+ if (error) {
85
+ promise.reject(error)
86
+ } else {
87
+ promise.resolve(token)
88
+ }
89
+ })
90
+ failedQueue = []
91
+ }
92
+
93
+ // ─── Request Interceptor ──────────────────────────────────────────────────────
94
+ api.interceptors.request.use(
95
+ (config: CustomInternalRequestConfig) => {
96
+ // 1. Trigger global loading bar
97
+ if (!config.skipLoading) {
98
+ loadingBar.start()
99
+ }
100
+
101
+ // 2. Attach Authorization token
102
+ if (!config.skipAuth) {
103
+ const token = tokenStorage.getAccessToken()
104
+ if (token && config.headers) {
105
+ config.headers.Authorization = `Bearer ${token}`
106
+ }
107
+ }
108
+
109
+ return config
110
+ },
111
+ (error: AxiosError) => {
112
+ loadingBar.fail()
113
+ return Promise.reject(error)
114
+ },
115
+ )
116
+
117
+ // ─── Response Interceptor ─────────────────────────────────────────────────────
118
+ api.interceptors.response.use(
119
+ (response: AxiosResponse) => {
120
+ const config = response.config as CustomInternalRequestConfig
121
+ if (!config.skipLoading) {
122
+ loadingBar.finish()
123
+ }
124
+ return response
125
+ },
126
+ async (error: AxiosError<{ message?: string; error?: string }>) => {
127
+ const originalRequest = error.config as CustomInternalRequestConfig | undefined
128
+ const status = error.response?.status
129
+
130
+ if (!originalRequest?.skipLoading) {
131
+ loadingBar.fail()
132
+ }
133
+
134
+ // ─── 1. Handle 401 Unauthorized & Silent Token Refresh ─────────────────
135
+ if (status === 401 && originalRequest && !originalRequest._retry) {
136
+ const refreshToken = tokenStorage.getRefreshToken()
137
+
138
+ // If no refresh token available or request was to auth/refresh itself
139
+ if (!refreshToken || originalRequest.url?.includes('/auth/refresh')) {
140
+ tokenStorage.clearTokens()
141
+ if (!originalRequest.skipToast) {
142
+ toast.error('Session expired. Please log in again.')
143
+ }
144
+ if (window.location.pathname !== '/auth/login') {
145
+ window.location.href = `/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`
146
+ }
147
+ return Promise.reject(error)
148
+ }
149
+
150
+ if (isRefreshing) {
151
+ // Queue this request while refresh is in flight
152
+ return new Promise((resolve, reject) => {
153
+ failedQueue.push({ resolve, reject })
154
+ })
155
+ .then((token) => {
156
+ if (originalRequest.headers) {
157
+ originalRequest.headers.Authorization = `Bearer ${token}`
158
+ }
159
+ return api(originalRequest)
160
+ })
161
+ .catch((err) => Promise.reject(err))
162
+ }
163
+
164
+ originalRequest._retry = true
165
+ isRefreshing = true
166
+
167
+ try {
168
+ // Execute token refresh call
169
+ const response = await axios.post<{
170
+ accessToken: string
171
+ refreshToken?: string
172
+ }>(
173
+ `${import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'}/auth/refresh`,
174
+ { refreshToken },
175
+ { headers: { 'Content-Type': 'application/json' } },
176
+ )
177
+
178
+ const { accessToken, refreshToken: newRefreshToken } = response.data
179
+ tokenStorage.setAccessToken(accessToken)
180
+ if (newRefreshToken) {
181
+ tokenStorage.setRefreshToken(newRefreshToken)
182
+ }
183
+
184
+ if (originalRequest.headers) {
185
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`
186
+ }
187
+
188
+ processQueue(null, accessToken)
189
+ return api(originalRequest)
190
+ } catch (refreshError) {
191
+ processQueue(refreshError as AxiosError, null)
192
+ tokenStorage.clearTokens()
193
+ toast.error('Session has ended. Please sign in again.')
194
+ window.location.href = '/auth/login'
195
+ return Promise.reject(refreshError)
196
+ } finally {
197
+ isRefreshing = false
198
+ }
199
+ }
200
+
201
+ // ─── 2. Handle 403 Forbidden ──────────────────────────────────────────
202
+ if (status === 403) {
203
+ if (!originalRequest?.skipToast) {
204
+ toast.error('Access denied. You do not have permission for this action.')
205
+ }
206
+ }
207
+
208
+ // ─── 3. Handle 500+ Server Errors & Network Errors ────────────────────
209
+ if (!originalRequest?.skipToast) {
210
+ const serverMessage =
211
+ error.response?.data?.message ||
212
+ error.response?.data?.error ||
213
+ (status ? `Server error (${status}). Please try again later.` : 'Network connection failed. Please check your internet.')
214
+
215
+ if (status !== 401 && status !== 403) {
216
+ toast.error(serverMessage)
217
+ }
218
+ }
219
+
220
+ return Promise.reject(error)
221
+ },
222
+ )
223
+
224
+ // ─── Typed API Client Helper Wrapper ──────────────────────────────────────────
225
+ export const apiClient = {
226
+ get<T = unknown>(url: string, config?: CustomRequestConfig): Promise<T> {
227
+ return api.get<T, AxiosResponse<T>>(url, config).then((res) => res.data)
228
+ },
229
+ post<T = unknown>(url: string, data?: unknown, config?: CustomRequestConfig): Promise<T> {
230
+ return api.post<T, AxiosResponse<T>>(url, data, config).then((res) => res.data)
231
+ },
232
+ put<T = unknown>(url: string, data?: unknown, config?: CustomRequestConfig): Promise<T> {
233
+ return api.put<T, AxiosResponse<T>>(url, data, config).then((res) => res.data)
234
+ },
235
+ patch<T = unknown>(url: string, data?: unknown, config?: CustomRequestConfig): Promise<T> {
236
+ return api.patch<T, AxiosResponse<T>>(url, data, config).then((res) => res.data)
237
+ },
238
+ delete<T = unknown>(url: string, config?: CustomRequestConfig): Promise<T> {
239
+ return api.delete<T, AxiosResponse<T>>(url, config).then((res) => res.data)
240
+ },
241
+ }
242
+
243
+ export default api
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import {
3
+ formatCurrency,
4
+ formatNumber,
5
+ formatCompactNumber,
6
+ formatDate,
7
+ formatDateTime,
8
+ formatRelativeTime,
9
+ formatBytes,
10
+ getInitials,
11
+ truncate,
12
+ } from './formatters'
13
+
14
+ describe('Formatters Utility', () => {
15
+ describe('formatCurrency', () => {
16
+ it('formats USD currency by default', () => {
17
+ const result = formatCurrency(1250.5)
18
+ expect(result).toBe('$1,250.50')
19
+ })
20
+
21
+ it('formats IDR without decimals by default', () => {
22
+ const result = formatCurrency(50000, { currency: 'IDR', locale: 'id-ID' })
23
+ expect(result.replace(/\s/g, ' ')).toMatch(/Rp\s?50\.000/)
24
+ })
25
+
26
+ it('handles null, undefined, empty, or invalid inputs safely', () => {
27
+ expect(formatCurrency(null)).toBe('-')
28
+ expect(formatCurrency(undefined)).toBe('-')
29
+ expect(formatCurrency('')).toBe('-')
30
+ expect(formatCurrency('not-a-number')).toBe('-')
31
+ })
32
+ })
33
+
34
+ describe('formatNumber', () => {
35
+ it('formats numbers with thousand separators', () => {
36
+ expect(formatNumber(1000000)).toBe('1,000,000')
37
+ expect(formatNumber('4500.5')).toBe('4,500.5')
38
+ })
39
+
40
+ it('returns "-" for invalid numbers', () => {
41
+ expect(formatNumber(null)).toBe('-')
42
+ expect(formatNumber('invalid')).toBe('-')
43
+ })
44
+ })
45
+
46
+ describe('formatCompactNumber', () => {
47
+ it('formats large numbers into compact strings', () => {
48
+ expect(formatCompactNumber(1500)).toBe('1.5K')
49
+ expect(formatCompactNumber(2500000)).toBe('2.5M')
50
+ })
51
+
52
+ it('handles small numbers', () => {
53
+ expect(formatCompactNumber(120)).toBe('120')
54
+ })
55
+ })
56
+
57
+ describe('formatDate and formatDateTime', () => {
58
+ it('formats dates into readable format', () => {
59
+ const date = new Date('2026-08-21T07:00:00Z')
60
+ expect(formatDate(date, 'medium')).toContain('2026')
61
+ expect(formatDateTime(date)).toContain('2026')
62
+ })
63
+
64
+ it('returns "-" for invalid dates', () => {
65
+ expect(formatDate(null)).toBe('-')
66
+ expect(formatDate('invalid-date')).toBe('-')
67
+ expect(formatDateTime(undefined)).toBe('-')
68
+ })
69
+ })
70
+
71
+ describe('formatRelativeTime', () => {
72
+ beforeEach(() => {
73
+ vi.useFakeTimers()
74
+ vi.setSystemTime(new Date('2026-08-21T12:00:00Z'))
75
+ })
76
+
77
+ afterEach(() => {
78
+ vi.useRealTimers()
79
+ })
80
+
81
+ it('returns "just now" for dates within 45 seconds', () => {
82
+ const date = new Date('2026-08-21T11:59:30Z')
83
+ expect(formatRelativeTime(date)).toBe('just now')
84
+ })
85
+
86
+ it('returns relative minutes, hours, or days', () => {
87
+ const fiveMinsAgo = new Date('2026-08-21T11:55:00Z')
88
+ expect(formatRelativeTime(fiveMinsAgo)).toBe('5 minutes ago')
89
+
90
+ const twoHoursAgo = new Date('2026-08-21T10:00:00Z')
91
+ expect(formatRelativeTime(twoHoursAgo)).toBe('2 hours ago')
92
+ })
93
+ })
94
+
95
+ describe('formatBytes', () => {
96
+ it('formats bytes into KB, MB, GB accurately', () => {
97
+ expect(formatBytes(0)).toBe('0 B')
98
+ expect(formatBytes(1024)).toBe('1 KB')
99
+ expect(formatBytes(1048576)).toBe('1 MB')
100
+ expect(formatBytes(1073741824)).toBe('1 GB')
101
+ })
102
+
103
+ it('handles null/invalid bytes', () => {
104
+ expect(formatBytes(null)).toBe('-')
105
+ expect(formatBytes(undefined)).toBe('-')
106
+ })
107
+ })
108
+
109
+ describe('getInitials', () => {
110
+ it('extracts two letter initials from full names', () => {
111
+ expect(getInitials('Olivia Martin')).toBe('OM')
112
+ expect(getInitials('John')).toBe('JO')
113
+ expect(getInitials('Alexander Graham Bell')).toBe('AB')
114
+ })
115
+
116
+ it('returns fallback NA for empty or invalid values', () => {
117
+ expect(getInitials('')).toBe('NA')
118
+ expect(getInitials(null)).toBe('NA')
119
+ expect(getInitials(undefined)).toBe('NA')
120
+ })
121
+ })
122
+
123
+ describe('truncate', () => {
124
+ it('truncates text that exceeds maximum length', () => {
125
+ expect(truncate('Hello world this is a test', 10)).toBe('Hello worl...')
126
+ })
127
+
128
+ it('keeps text untouched if below limit', () => {
129
+ expect(truncate('Short', 10)).toBe('Short')
130
+ })
131
+
132
+ it('handles empty strings safely', () => {
133
+ expect(truncate('')).toBe('')
134
+ expect(truncate(null)).toBe('')
135
+ })
136
+ })
137
+ })