create-nexgen 1.0.4

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 (240) hide show
  1. package/package.json +26 -0
  2. package/src/index.js +108 -0
  3. package/template/.dockerignore +14 -0
  4. package/template/.env +58 -0
  5. package/template/.env.example +59 -0
  6. package/template/.prettierignore +5 -0
  7. package/template/.prettierrc +8 -0
  8. package/template/README.md +447 -0
  9. package/template/drizzle.config.ts +29 -0
  10. package/template/eslint.config.js +52 -0
  11. package/template/gitignore-stub +24 -0
  12. package/template/package.json +96 -0
  13. package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
  14. package/template/public/assets/Button-_7aQ7gHL.js +1 -0
  15. package/template/public/assets/Input-CLNJXmKc.css +1 -0
  16. package/template/public/assets/Input-z8GI8Aqo.js +1 -0
  17. package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
  18. package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
  19. package/template/public/assets/Layout-DotR1sQC.js +1 -0
  20. package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
  21. package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
  22. package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
  23. package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  24. package/template/public/assets/dashboard-CwybEyLc.js +1 -0
  25. package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
  26. package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
  27. package/template/public/assets/index-Bleyx5dm.js +64 -0
  28. package/template/public/assets/index-DUw8E6Yg.css +1 -0
  29. package/template/public/assets/login-DC7PTlQF.js +1 -0
  30. package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
  31. package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
  32. package/template/public/assets/register-3O7Qs28C.js +1 -0
  33. package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
  34. package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
  35. package/template/public/index.html +17 -0
  36. package/template/src/database/migrations/mysql/0000_init.sql +73 -0
  37. package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
  38. package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
  39. package/template/src/database/schema.ts +4 -0
  40. package/template/src/env.ts +107 -0
  41. package/template/src/framework/cache/cache.ts +81 -0
  42. package/template/src/framework/database/connection.ts +168 -0
  43. package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
  44. package/template/src/framework/database/paginate.ts +200 -0
  45. package/template/src/framework/database/schema.ts +26 -0
  46. package/template/src/framework/database/seed.ts +33 -0
  47. package/template/src/framework/events/dispatcher.ts +57 -0
  48. package/template/src/framework/facade.ts +27 -0
  49. package/template/src/framework/http/app.ts +61 -0
  50. package/template/src/framework/http/cors.ts +19 -0
  51. package/template/src/framework/http/logger.ts +85 -0
  52. package/template/src/framework/http/openapi.ts +34 -0
  53. package/template/src/framework/http/ratelimiter.ts +13 -0
  54. package/template/src/framework/http/router.ts +76 -0
  55. package/template/src/framework/http/static.ts +33 -0
  56. package/template/src/framework/http/validation.ts +24 -0
  57. package/template/src/framework/kernel.ts +40 -0
  58. package/template/src/framework/maker-cli/src/index.mjs +51 -0
  59. package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
  60. package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
  61. package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
  62. package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
  63. package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
  64. package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
  65. package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
  66. package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
  67. package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
  68. package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
  69. package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
  70. package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
  71. package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
  72. package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
  73. package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
  74. package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
  75. package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
  76. package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
  77. package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
  78. package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
  79. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
  80. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
  81. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
  82. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
  83. package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
  84. package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
  85. package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
  86. package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
  87. package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
  88. package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
  89. package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
  90. package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
  91. package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
  92. package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
  93. package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
  94. package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
  95. package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
  96. package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
  97. package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
  98. package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
  99. package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
  100. package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
  101. package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
  102. package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
  103. package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
  104. package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
  105. package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
  106. package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
  107. package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
  108. package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
  109. package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
  110. package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
  111. package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
  112. package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
  113. package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
  114. package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
  115. package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
  116. package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
  117. package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
  118. package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
  119. package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
  120. package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
  121. package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
  122. package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
  123. package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
  124. package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
  125. package/template/src/framework/modules/discover.ts +54 -0
  126. package/template/src/framework/modules/routes.ts +26 -0
  127. package/template/src/framework/notification/index.ts +109 -0
  128. package/template/src/framework/queue/clear.ts +20 -0
  129. package/template/src/framework/queue/queue.ts +213 -0
  130. package/template/src/framework/queue/ui.ts +104 -0
  131. package/template/src/framework/queue/worker.ts +33 -0
  132. package/template/src/framework/realtime/broadcast.ts +27 -0
  133. package/template/src/framework/realtime/index.ts +1 -0
  134. package/template/src/framework/realtime/socket-cookie.ts +65 -0
  135. package/template/src/framework/realtime/socket.ts +132 -0
  136. package/template/src/framework/realtime/types.ts +6 -0
  137. package/template/src/framework/realtime/ui.ts +16 -0
  138. package/template/src/framework/redis/client.ts +126 -0
  139. package/template/src/framework/scheduler/lock.ts +124 -0
  140. package/template/src/framework/scheduler/run.ts +26 -0
  141. package/template/src/framework/scheduler/scheduler.ts +82 -0
  142. package/template/src/framework/server.ts +147 -0
  143. package/template/src/framework/session/session.ts +116 -0
  144. package/template/src/framework/storage/storage.ts +743 -0
  145. package/template/src/framework/support/cookie.ts +78 -0
  146. package/template/src/framework/support/jwt.ts +45 -0
  147. package/template/src/framework/support/lifecycle.ts +35 -0
  148. package/template/src/framework/support/logger.ts +102 -0
  149. package/template/src/framework/support/mail.ts +43 -0
  150. package/template/src/framework/support/password.ts +23 -0
  151. package/template/src/framework/support/url.ts +25 -0
  152. package/template/src/middlewares/auth-middleware.ts +98 -0
  153. package/template/src/middlewares/role-middleware.ts +24 -0
  154. package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
  155. package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
  156. package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
  157. package/template/src/modules/auth/controllers/role.controller.ts +25 -0
  158. package/template/src/modules/auth/database/models/notifications.ts +22 -0
  159. package/template/src/modules/auth/database/models/role.ts +14 -0
  160. package/template/src/modules/auth/database/models/user.ts +46 -0
  161. package/template/src/modules/auth/database/seeders/role.ts +19 -0
  162. package/template/src/modules/auth/database/seeders/user.ts +33 -0
  163. package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
  164. package/template/src/modules/auth/jobs/registeruser.ts +31 -0
  165. package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
  166. package/template/src/modules/auth/routes/api.ts +151 -0
  167. package/template/src/modules/auth/routes/role.ts +39 -0
  168. package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
  169. package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
  170. package/template/src/modules/welcome/database/models/welcome.ts +6 -0
  171. package/template/src/modules/welcome/routes/api.ts +20 -0
  172. package/template/src/resources/index.html +16 -0
  173. package/template/src/resources/src/App.vue +5 -0
  174. package/template/src/resources/src/assets/css/styles.css +14934 -0
  175. package/template/src/resources/src/assets/css/styles.css.map +1 -0
  176. package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
  177. package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
  178. package/template/src/resources/src/assets/images/logo-1.png +0 -0
  179. package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
  180. package/template/src/resources/src/assets/images/logo-dark.png +0 -0
  181. package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
  182. package/template/src/resources/src/assets/images/logo-sm.png +0 -0
  183. package/template/src/resources/src/assets/images/logo1.png +0 -0
  184. package/template/src/resources/src/assets/images/logo2.png +0 -0
  185. package/template/src/resources/src/assets/scss/custom.css +217 -0
  186. package/template/src/resources/src/assets/scss/custom.css.map +1 -0
  187. package/template/src/resources/src/assets/scss/custom.scss +1100 -0
  188. package/template/src/resources/src/components/Button.vue +35 -0
  189. package/template/src/resources/src/components/Checkbox.vue +29 -0
  190. package/template/src/resources/src/components/FloatButton.vue +36 -0
  191. package/template/src/resources/src/components/Href.vue +32 -0
  192. package/template/src/resources/src/components/Input.vue +227 -0
  193. package/template/src/resources/src/components/InputGroup.vue +153 -0
  194. package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
  195. package/template/src/resources/src/components/Modal.vue +102 -0
  196. package/template/src/resources/src/components/Pagebar.vue +28 -0
  197. package/template/src/resources/src/components/Refresh.vue +26 -0
  198. package/template/src/resources/src/components/Select.vue +390 -0
  199. package/template/src/resources/src/components/Spinner.vue +42 -0
  200. package/template/src/resources/src/components/Switch.vue +65 -0
  201. package/template/src/resources/src/components/TextArea.vue +121 -0
  202. package/template/src/resources/src/components/Toast.vue +56 -0
  203. package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
  204. package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
  205. package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
  206. package/template/src/resources/src/components/datatable/index.vue +237 -0
  207. package/template/src/resources/src/composables/useAuth.ts +52 -0
  208. package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
  209. package/template/src/resources/src/composables/useDialog.ts +5 -0
  210. package/template/src/resources/src/composables/useGum.ts +3 -0
  211. package/template/src/resources/src/composables/usePulse.ts +5 -0
  212. package/template/src/resources/src/env.d.ts +20 -0
  213. package/template/src/resources/src/helpers/nformatter.ts +10 -0
  214. package/template/src/resources/src/helpers/utils.ts +68 -0
  215. package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
  216. package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
  217. package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
  218. package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
  219. package/template/src/resources/src/layouts/Layout/index.vue +76 -0
  220. package/template/src/resources/src/main.ts +27 -0
  221. package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
  222. package/template/src/resources/src/pages/auth/login.vue +93 -0
  223. package/template/src/resources/src/pages/auth/register.vue +130 -0
  224. package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
  225. package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
  226. package/template/src/resources/src/pages/dashboard/index.vue +76 -0
  227. package/template/src/resources/src/plugins/axios.ts +33 -0
  228. package/template/src/resources/src/plugins/browserDetect.ts +55 -0
  229. package/template/src/resources/src/plugins/dialog.ts +167 -0
  230. package/template/src/resources/src/plugins/gum.ts +343 -0
  231. package/template/src/resources/src/plugins/pulse.ts +141 -0
  232. package/template/src/resources/src/plugins/routeProgress.ts +87 -0
  233. package/template/src/resources/src/router/index.ts +85 -0
  234. package/template/src/resources/src/stores/admin-ui.ts +148 -0
  235. package/template/src/resources/src/stores/auth.ts +151 -0
  236. package/template/src/resources/tsconfig.json +19 -0
  237. package/template/src/resources/vite.config.ts +43 -0
  238. package/template/src/storage/logs/app.log +20179 -0
  239. package/template/src/storage/logs/fatal.log +727 -0
  240. package/template/tsconfig.json +20 -0
@@ -0,0 +1,390 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ ref,
4
+ reactive,
5
+ onMounted,
6
+ nextTick,
7
+ computed,
8
+ onBeforeUnmount,
9
+ watch,
10
+ useAttrs
11
+ } from "vue";
12
+ import { debounce } from "lodash-es";
13
+ import axios from "axios";
14
+ import vSelect from "vue-select";
15
+ import "vue-select/dist/vue-select.css";
16
+ import { empty } from "../helpers/utils";
17
+
18
+ defineOptions({ name: "Select", inheritAttrs: false });
19
+
20
+ type AnyRecord = Record<string, unknown>;
21
+ type SelectValue = string | number | boolean | AnyRecord | null;
22
+ type OptionHook = ((value: unknown) => void | Promise<void>) | null;
23
+
24
+ interface FetchPack {
25
+ url?: string | null;
26
+ data?: string | null;
27
+ params?: AnyRecord;
28
+ mapFn?: (x: AnyRecord) => AnyRecord;
29
+ option?:
30
+ | ((value: unknown) => void | Promise<void>)
31
+ | Array<(value: unknown) => void | Promise<void>>;
32
+ reset?: boolean;
33
+ reload?: boolean;
34
+ }
35
+
36
+ interface SelectProps {
37
+ fetched?: (payload: {
38
+ search: string;
39
+ reset?: boolean;
40
+ reload?: boolean;
41
+ }) => Promise<FetchPack | null | undefined> | FetchPack | null | undefined;
42
+ must?: boolean;
43
+ err?: string | boolean;
44
+ hood?: string | boolean;
45
+ defaultValue?: SelectValue;
46
+ resetKey?: string | number | boolean | AnyRecord | null;
47
+ }
48
+
49
+ const $attrs = useAttrs();
50
+ const props = withDefaults(defineProps<SelectProps>(), {
51
+ defaultValue: null,
52
+ resetKey: null
53
+ });
54
+
55
+ // vue3.4 way
56
+ const emit = defineEmits<{
57
+ (event: "fetched"): void;
58
+ (event: "clear"): void;
59
+ }>();
60
+ const model = defineModel<SelectValue | SelectValue[]>();
61
+
62
+ const skipNextSearch = ref(false); // suppress search right after clear
63
+
64
+ let observer = ref<IntersectionObserver | null>(null);
65
+ let load = ref<HTMLElement | null>(null);
66
+
67
+ // store callbacks coming from fetchedDropdown(pack)
68
+ const hooks = ref<{ option: OptionHook | OptionHook[]; }>({ option: null }); // function | function[] | null
69
+
70
+ // remember last fetch config and search so the child can refetch/append by itself
71
+ const lastPack = ref<FetchPack | null>(null); // { url, data, params, mapFn }
72
+ const lastSearch = ref(""); // current search term
73
+
74
+ const hasNextPage = computed(() => {
75
+ const total = Number((fieldData.value as { total?: number; }).total ?? 0);
76
+ return field.all.length < total;
77
+ });
78
+
79
+ // ask parent for a pack and run our internal fetcher
80
+ const callParentFetched = async (search: string, opts: Partial<FetchPack> = {}) => {
81
+ const maybePack = await props.fetched?.({ search, ...opts });
82
+ if (maybePack && typeof maybePack === "object") {
83
+ await fetchedDropdown(search, { ...maybePack, ...opts });
84
+ }
85
+ };
86
+
87
+ const onClear = () => {
88
+ resetMe(); // clear options + selected value
89
+ skipNextSearch.value = true; // suppress the immediate empty search
90
+ emit("clear"); // notify parent that clear button was clicked
91
+ };
92
+
93
+ const onOpen = async () => {
94
+ // if empty, load page 1 via parent fetcher (no boolean flags)
95
+ if (!field.all.length) {
96
+ if (lastPack.value) {
97
+ await fetchedDropdown("", { ...lastPack.value, reset: true });
98
+ } else {
99
+ // await props.fetched?.('');
100
+ await callParentFetched("", { reset: true });
101
+ }
102
+ }
103
+
104
+ await nextTick();
105
+ if (load.value && hasNextPage.value) {
106
+ observer.value?.observe(load.value);
107
+ }
108
+ };
109
+
110
+ const onClose = async () => {
111
+ observer.value?.disconnect();
112
+ };
113
+
114
+ const onModelUpdate = async (val: unknown) => {
115
+ const optHooks = Array.isArray(hooks.value.option)
116
+ ? hooks.value.option
117
+ : hooks.value.option
118
+ ? [hooks.value.option]
119
+ : [];
120
+
121
+ // When user clicks × or backspaces everything, v-select sets model to null.
122
+ if (val == null || val === "") {
123
+ for (const f of optHooks) {
124
+ if (typeof f === "function") {
125
+ await Promise.resolve(f(null));
126
+ }
127
+ }
128
+ emit("clear");
129
+ return;
130
+ }
131
+
132
+ field.val = val as SelectValue;
133
+
134
+ for (const f of optHooks) {
135
+ if (typeof f === "function") {
136
+ await Promise.resolve(f(val));
137
+ }
138
+ }
139
+ };
140
+
141
+ const inputSearch = debounce(async (search: string, loading: (value: boolean) => void) => {
142
+ if (skipNextSearch.value && (!search || !search.trim().length)) {
143
+ skipNextSearch.value = false; // consume the flag
144
+ return;
145
+ }
146
+
147
+ if (search && search.trim().length) {
148
+ loading(true);
149
+ if (lastPack.value) {
150
+ await fetchedDropdown(search, { ...lastPack.value, reset: true }).finally(() =>
151
+ loading(false)
152
+ );
153
+ } else {
154
+ await callParentFetched(search, { reset: true }).finally(() => loading(false));
155
+ }
156
+ } else {
157
+ // user erased the query manually
158
+ field.page = 0;
159
+ if (lastPack.value) {
160
+ await fetchedDropdown("", { ...lastPack.value }); // no reset → keep list
161
+ } else {
162
+ await callParentFetched("");
163
+ }
164
+ await nextTick();
165
+ if (load.value && hasNextPage.value) observer.value?.observe(load.value);
166
+ }
167
+ }, 400);
168
+
169
+ const infiniteScroll = async ([{ isIntersecting, target }]: IntersectionObserverEntry[]) => {
170
+ if (!isIntersecting) return;
171
+ const ul =
172
+ target instanceof HTMLElement && target.offsetParent instanceof HTMLElement
173
+ ? target.offsetParent
174
+ : null;
175
+ const scrollTop = ul?.scrollTop ?? 0;
176
+ if (lastPack.value) {
177
+ await fetchedDropdown(lastSearch.value, { ...lastPack.value }); // next page of current mode
178
+ } else {
179
+ await callParentFetched(lastSearch.value || "");
180
+ }
181
+ await nextTick();
182
+ if (ul) ul.scrollTop = scrollTop;
183
+ };
184
+
185
+ onMounted(async () => {
186
+ observer.value = new IntersectionObserver(infiniteScroll);
187
+
188
+ // set default value if provided
189
+ if (props.defaultValue) {
190
+ await nextTick();
191
+ field.val = props.defaultValue;
192
+ model.value = props.defaultValue;
193
+ }
194
+ });
195
+
196
+ onBeforeUnmount(() => {
197
+ observer.value?.disconnect();
198
+ observer.value = null;
199
+ });
200
+
201
+ // prototype and it will call from parent
202
+ const field = reactive<{ all: AnyRecord[]; val: SelectValue; page: number; size: number; }>({
203
+ all: [],
204
+ val: "",
205
+ page: 0,
206
+ size: 10
207
+ });
208
+ const fieldData = ref<AnyRecord>({});
209
+
210
+ const resetMe = () => {
211
+ field.all = [];
212
+ field.page = 0;
213
+ field.val = "";
214
+ };
215
+
216
+ const reloadMe = () => {
217
+ field.all = [];
218
+ field.page = 0;
219
+ };
220
+
221
+ const fetchedDropdown = async (search: string, pack: FetchPack | null = null) => {
222
+ const url = pack?.url ?? null;
223
+ const data = pack?.data ?? null;
224
+ const params = pack?.params ?? {};
225
+ const mapFn = pack?.mapFn ?? ((x: AnyRecord) => x);
226
+ const reset = !!pack?.reset;
227
+ const reload = !!pack?.reload;
228
+
229
+ // make option sticky: only overwrite if caller provided it
230
+ if (Object.prototype.hasOwnProperty.call(pack ?? {}, "option")) {
231
+ const pOpt = pack?.option;
232
+ hooks.value.option = Array.isArray(pOpt)
233
+ ? pOpt.filter((fn) => typeof fn === "function")
234
+ : typeof pOpt === "function"
235
+ ? [pOpt]
236
+ : [];
237
+ }
238
+
239
+ // remember essentials so the child can refetch/append on its own
240
+ lastPack.value = { url, data, params, mapFn };
241
+ lastSearch.value = search ?? "";
242
+
243
+ // if pass and true -> reset, reload , search
244
+ (reset && resetMe()) ||
245
+ (reload && reloadMe()) ||
246
+ (!empty(search) && !reset && !reload && reloadMe());
247
+
248
+ // after reset/reload/search
249
+ field.page++;
250
+ const param = { page: field.page, size: field.size, search: search };
251
+
252
+ // log once (don't call axios twice)
253
+ // console.log('GET', pack.url, { params: pack.params ? { ...pack.params, ...param } : param });
254
+
255
+ if (!url || !data) {
256
+ return;
257
+ }
258
+
259
+ const key = await axios.get(url, { params: params ? { ...params, ...param } : param });
260
+ const payload = key.data[data as string] as AnyRecord;
261
+
262
+ fieldData.value = payload;
263
+ const rows = Array.isArray((payload as { data?: unknown; }).data)
264
+ ? (payload as { data: AnyRecord[]; }).data
265
+ : [];
266
+ // console.log('Fetched', payload?.data);
267
+ rows.forEach((dt) => {
268
+ const opt = mapFn(dt);
269
+ const key = opt?.id ?? opt?.title ?? JSON.stringify(opt); // build a comparison key: prefer id, then title, then full object
270
+
271
+ if (
272
+ !field.all.some(
273
+ (o) =>
274
+ ((o as { id?: unknown; title?: unknown; }).id ??
275
+ (o as { id?: unknown; title?: unknown; }).title ??
276
+ JSON.stringify(o)) === key
277
+ )
278
+ ) {
279
+ field.all = [...field.all, opt];
280
+ }
281
+ });
282
+ };
283
+
284
+ /**
285
+ * This function created for
286
+ * functionally reset or reload
287
+ * from without component
288
+ * like fn.value.reload({reset:true/reload:true/both});
289
+ * it is exposed, so we can call from with
290
+ * template ref
291
+ */
292
+ const reload = async () => {
293
+ resetMe(); // clear field.all, page, val
294
+ await callParentFetched("", { reset: true }); // ask parent to fetch first page again
295
+ };
296
+
297
+ /**
298
+ * if backend send proper object or null
299
+ * then watch function not get any axios
300
+ * error else axios can send error
301
+ */
302
+ watch(
303
+ () => props.resetKey,
304
+ async (newVal, oldVal) => {
305
+ if (oldVal === undefined) return;
306
+
307
+ const optHooks = Array.isArray(hooks.value.option)
308
+ ? hooks.value.option
309
+ : hooks.value.option
310
+ ? [hooks.value.option]
311
+ : [];
312
+
313
+ resetMe(); // clears field.all, field.page, field.val
314
+ model.value = "";
315
+
316
+ // Notify hooks that selection is gone
317
+ for (const fn of optHooks) {
318
+ if (typeof fn === "function") {
319
+ await Promise.resolve(fn(null));
320
+ }
321
+ }
322
+
323
+ skipNextSearch.value = true; // avoid instant empty-search call
324
+ await callParentFetched("", { reset: true, reload: true });
325
+ }
326
+ );
327
+
328
+ defineExpose({ field, fieldData, fetchedDropdown, reload });
329
+
330
+ const parentClass = computed(() => ($attrs.parentclass as string | undefined) || "mb-2");
331
+ const inputId = computed(() => ($attrs.id as string | undefined) || "");
332
+ const inputTitle = computed(() => ($attrs.title as string | undefined) || "");
333
+ const hoodHtml = computed(() =>
334
+ props.hood === false || props.hood == null ? "" : String(props.hood)
335
+ );
336
+ </script>
337
+
338
+ <template>
339
+ <div :class="parentClass">
340
+ <label
341
+ :for="inputId"
342
+ class="text-capitalize d-flex align-items-center mb-1"
343
+ :class="{ 'd-none': !inputTitle }">
344
+ <div class="position-relative">
345
+ <span class="text-capitalize" v-html="inputTitle"></span>
346
+ <span
347
+ class="position-absolute text-danger rounded-circle bg-danger must"
348
+ :class="{ 'd-none': !props.must }"></span>
349
+ </div>
350
+ <div
351
+ class="text-uppercase w-100 text-end text-primary fw-semibold"
352
+ :class="{ 'd-none': !props.hood }"
353
+ style="font-size: 12px; margin-top: 0.15rem"
354
+ v-html="hoodHtml"></div>
355
+ </label>
356
+ <v-select
357
+ v-model="model"
358
+ label="title"
359
+ :filterable="false"
360
+ :options="field.all"
361
+ v-bind="$attrs"
362
+ @open="onOpen"
363
+ @update:model-value="onModelUpdate"
364
+ @close="onClose"
365
+ @search="inputSearch"
366
+ @clear="onClear">
367
+ <template #list-footer>
368
+ <li v-show="hasNextPage" ref="load" class="loader">More...</li>
369
+ </template>
370
+ </v-select>
371
+ <div class="form-text text-danger" :class="{ 'd-none': !props.err }" style="font-size: 0.8rem">
372
+ {{ props.err }}
373
+ </div>
374
+ </div>
375
+ </template>
376
+
377
+ <style lang="scss" scoped>
378
+ .loader {
379
+ text-align: center;
380
+ color: #bbbbbb;
381
+ }
382
+
383
+ .must {
384
+ width: 4px;
385
+ height: 4px;
386
+ top: 0;
387
+ margin-top: 5px;
388
+ margin-left: 2px;
389
+ }
390
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div class="h-100" v-bind="$attrs">
3
+ <div class="h-100 d-flex align-items-center justify-content-center">
4
+ <div class="spinner-grow" :style="spinnerStyle" role="status">
5
+ <span class="visually-hidden">Loading...</span>
6
+ </div>
7
+ <div class="fw-bold ms-2">
8
+ <span v-if="props.text" :class="props.textsize" :style="textStyle">
9
+ {{ props.text }}
10
+ </span>
11
+ <span v-else>Loading...</span>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed } from "vue";
19
+
20
+ defineOptions({ name: "Spinner", inheritAttrs: false });
21
+
22
+ interface SpinnerProps {
23
+ size?: string;
24
+ text?: string;
25
+ textsize?: string;
26
+ }
27
+
28
+ const props = withDefaults(defineProps<SpinnerProps>(), {
29
+ size: "3rem",
30
+ text: "",
31
+ textsize: ""
32
+ });
33
+
34
+ const spinnerStyle = computed(() => ({
35
+ width: props.size,
36
+ height: props.size
37
+ }));
38
+
39
+ const textStyle = computed(() => ({
40
+ fontSize: props.textsize || undefined
41
+ }));
42
+ </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <div :class="wrapperClass">
3
+ <label :class="props.vertical ? 'form-check-label d-block' : 'd-none'">
4
+ <span v-html="statusHtml"></span>
5
+ </label>
6
+ <div class="form-check form-switch d-inline-block">
7
+ <input
8
+ class="form-check-input shadow-none border-primary"
9
+ type="checkbox"
10
+ role="switch"
11
+ :checked="isChecked"
12
+ v-bind="$attrs" />
13
+ <label :class="props.vertical ? 'd-none' : 'form-check-label'">
14
+ <span v-html="statusHtml"></span>
15
+ </label>
16
+ </div>
17
+ </div>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { computed, useAttrs } from "vue";
22
+
23
+ defineOptions({ name: "Switch", inheritAttrs: false });
24
+
25
+ interface SwitchProps {
26
+ modelValue?: string | number | boolean;
27
+ value?: unknown[] | Record<string, unknown> | string | number | boolean;
28
+ vertical?: boolean;
29
+ checked?: boolean | number;
30
+ }
31
+
32
+ const $attrs = useAttrs();
33
+ const props = defineProps<SwitchProps>();
34
+
35
+ const textLabel = computed(() => ($attrs.text as string | undefined) || "");
36
+ const blankLabel = computed(() => ($attrs.blank as string | undefined) || "");
37
+ const topclass = computed(() => ($attrs.topclass as string | undefined) || "");
38
+
39
+ const isChecked = computed(() => Boolean(props.checked));
40
+
41
+ const wrapperClass = computed(() => (props.vertical ? `text-center ${topclass.value}`.trim() : ""));
42
+
43
+ const statusHtml = computed(() => {
44
+ if (isChecked.value) {
45
+ return `<div class='d-flex justify-content-center'><span>${textLabel.value}</span><div class='rounded-circle bg-success indicator'></div></div>`;
46
+ }
47
+
48
+ return `<div class='d-flex justify-content-center'><span>${blankLabel.value}</span><div class='rounded-circle bg-danger indicator'></div></div>`;
49
+ });
50
+ </script>
51
+
52
+ <style lang="scss">
53
+ .indicator {
54
+ width: 4px;
55
+ height: 4px;
56
+ margin-left: 2px;
57
+ margin-top: 5px;
58
+ }
59
+ </style>
60
+ <style lang="scss" scoped>
61
+ label,
62
+ input {
63
+ cursor: pointer;
64
+ }
65
+ </style>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div :class="wrapperClass">
3
+ <div class="position-relative">
4
+ <div class="position-relative mb-1">
5
+ <span class="text-capitalize" v-html="inputLabel"></span>
6
+ <span
7
+ class="position-absolute text-danger rounded-circle bg-danger must"
8
+ :class="{ 'd-none': !props.must }"></span>
9
+ </div>
10
+ <div class="form-floating h-100">
11
+ <textarea
12
+ :value="props.modelValue"
13
+ class="form-control mb-0 h-100"
14
+ :maxlength="maxLengthAttr"
15
+ :aria-describedby="`${inputId}Help`"
16
+ v-bind="$attrs"
17
+ @input="updateModel">
18
+ </textarea>
19
+ <label
20
+ :for="inputId"
21
+ class="d-block placeholder-label border-0"
22
+ :class="{ 'd-none': !placeholderText }">
23
+ {{ placeholderText }}
24
+ </label>
25
+ </div>
26
+ <label
27
+ class="position-absolute end-0 pe-1 text-secondary max"
28
+ :class="{ 'd-none': !max }"
29
+ style="top: 28px">
30
+ {{ maxCounter }}
31
+ </label>
32
+ </div>
33
+ <div :id="`${inputId}Help`" class="form-text text-danger" :class="{ 'd-none': !props.err }">
34
+ {{ props.err }}
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed, nextTick, onMounted, useAttrs } from "vue";
41
+
42
+ defineOptions({ name: "TextArea", inheritAttrs: false });
43
+
44
+ const $attrs = useAttrs();
45
+ interface TextAreaProps {
46
+ focus?: boolean;
47
+ must?: boolean;
48
+ err?: string | boolean;
49
+ modelValue?: string | number;
50
+ }
51
+
52
+ const props = withDefaults(defineProps<TextAreaProps>(), {
53
+ modelValue: ""
54
+ });
55
+
56
+ const inputId = computed(() => ($attrs.id as string | undefined) || "");
57
+ const inputLabel = computed(() => ($attrs.label as string | undefined) || "");
58
+ const placeholderText = computed(() => ($attrs.placeholder as string | undefined) || "");
59
+ const topclass = computed(() => ($attrs.topclass as string | undefined) || "");
60
+
61
+ const wrapperClass = computed(() =>
62
+ topclass.value ? `${topclass.value} position-relative` : "mb-2"
63
+ );
64
+
65
+ const max = computed<number | null>(() => {
66
+ const raw = $attrs.maxlength as string | number | undefined;
67
+ return raw != null ? Number(raw) : null;
68
+ });
69
+ const maxLengthAttr = computed<number | undefined>(() => max.value ?? undefined);
70
+ const maxCounter = computed(() =>
71
+ max.value == null ? "" : max.value - String(props.modelValue).length
72
+ );
73
+
74
+ const emit = defineEmits<{
75
+ (event: "update:modelValue", value: string): void;
76
+ }>();
77
+ const updateModel = (e: Event) => {
78
+ const target = e.target as HTMLTextAreaElement;
79
+ emit("update:modelValue", target.value);
80
+ };
81
+
82
+ onMounted(() => {
83
+ if (!props.focus || !inputId.value) {
84
+ return;
85
+ }
86
+
87
+ nextTick(() => {
88
+ const textarea = document.querySelector(`#${inputId.value}`) as HTMLTextAreaElement | null;
89
+ textarea?.focus();
90
+ });
91
+ });
92
+ </script>
93
+
94
+ <style lang="scss" scoped>
95
+ .form-floating {
96
+ label {
97
+ top: -1px;
98
+ left: 1px;
99
+ }
100
+
101
+ textarea:focus + label,
102
+ textarea:not(:placeholder-shown) + label {
103
+ top: -6px;
104
+ }
105
+ }
106
+
107
+ .placeholder-label {
108
+ color: #cccccc !important;
109
+ }
110
+
111
+ .must {
112
+ width: 4px;
113
+ height: 4px;
114
+ margin-left: 2px;
115
+ margin-top: 5px;
116
+ }
117
+
118
+ .max {
119
+ font-size: 0.6rem;
120
+ }
121
+ </style>
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <Teleport to="#toast-show">
3
+ <div
4
+ ref="notify"
5
+ class="toast position-fixed"
6
+ role="alert"
7
+ aria-live="assertive"
8
+ aria-atomic="true"
9
+ style="z-index: 1000000; top: 10px; right: 23px"
10
+ data-bs-delay="2000">
11
+ <div :class="`text-bg-${bgcolor} align-items-center rounded-1 shadow-sm`">
12
+ <div class="d-flex">
13
+ <div class="toast-body">
14
+ {{ message }}
15
+ </div>
16
+ <button
17
+ type="button"
18
+ class="btn-close btn-close-white me-2 m-auto"
19
+ data-bs-dismiss="toast"
20
+ aria-label="Close"></button>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </Teleport>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { ref } from "vue";
29
+ import { Toast } from "bootstrap";
30
+
31
+ defineOptions({ name: "Toast", inheritAttrs: false });
32
+
33
+ interface ToastProps {
34
+ icon?: string;
35
+ }
36
+
37
+ defineProps<ToastProps>();
38
+
39
+ const notify = ref<HTMLElement | null>(null);
40
+ const message = ref("");
41
+ const bgcolor = ref("primary");
42
+
43
+ const toastme = (msg: string, bg: string) => {
44
+ message.value = msg;
45
+ bgcolor.value = bg;
46
+ if (!notify.value) {
47
+ return;
48
+ }
49
+ const toast = Toast.getOrCreateInstance(notify.value);
50
+ toast.show();
51
+ };
52
+
53
+ defineExpose({ toastme });
54
+ </script>
55
+
56
+ <style lang="scss" scoped></style>