wad-lab 1.2.2 → 1.2.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 (207) hide show
  1. package/bin/index.js +477 -627
  2. package/package.json +1 -1
  3. package/templates/fullstack-springboot-vue/frontend/vue-axios-basic/src/App.vue +241 -429
  4. package/templates/fullstack-springboot-vue/frontend/vue-axios-basic/src/components/ItemFormModal.vue +183 -0
  5. package/templates/fullstack-springboot-vue/frontend/vue-axios-basic/src/components/ItemPatchModal.vue +193 -0
  6. package/templates/fullstack-springboot-vue/frontend/vue-axios-basic/src/components/ItemTable.vue +177 -0
  7. package/templates/fullstack-springboot-vue/frontend/vue-axios-pinia/src/App.vue +150 -441
  8. package/templates/fullstack-springboot-vue/frontend/vue-axios-pinia/src/components/ItemFormModal.vue +207 -0
  9. package/templates/fullstack-springboot-vue/frontend/vue-axios-pinia/src/components/ItemStats.vue +64 -0
  10. package/templates/fullstack-springboot-vue/frontend/vue-axios-pinia/src/components/ItemTable.vue +195 -0
  11. package/templates/fullstack-springboot-vue/frontend/vue-axios-router/src/App.vue +41 -90
  12. package/templates/fullstack-springboot-vue/frontend/vue-axios-router/src/components/ItemFormModal.vue +173 -0
  13. package/templates/fullstack-springboot-vue/frontend/vue-axios-router/src/components/ItemTable.vue +102 -0
  14. package/templates/fullstack-springboot-vue/frontend/vue-axios-router/src/components/NavBar.vue +57 -0
  15. package/templates/fullstack-springboot-vue/frontend/vue-axios-router/src/views/DepartmentView.vue +132 -296
  16. package/templates/fullstack-springboot-vue/frontend/vue-axios-router/src/views/EmployeeView.vue +126 -219
  17. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/components/ItemDetail.vue +115 -0
  18. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/components/ItemTable.vue +104 -0
  19. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/components/SideBar.vue +59 -0
  20. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/views/CompanyLayout.vue +2 -51
  21. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/views/DepartmentListView.vue +92 -250
  22. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/views/EmployeeDetailView.vue +59 -181
  23. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-children/src/views/EmployeeListView.vue +67 -315
  24. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/App.vue +19 -70
  25. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/components/ItemFormModal.vue +173 -0
  26. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/components/ItemPatchModal.vue +144 -0
  27. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/components/ItemTable.vue +102 -0
  28. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/components/NavBar.vue +53 -0
  29. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/views/DepartmentView.vue +107 -229
  30. package/templates/fullstack-springboot-vue/frontend/vue-axios-router-pinia/src/views/EmployeeView.vue +143 -257
  31. package/templates/game-bingo/script.js +1 -1
  32. package/templates/game-memory-cards/script.js +2 -2
  33. package/templates/game-rock-paper-scissors/index.html +3 -3
  34. package/templates/game-rock-paper-scissors/script.js +2 -2
  35. package/templates/game-tic-tac-toe/script.js +2 -2
  36. package/templates/game-whack-a-mole/index.html +9 -9
  37. package/templates/servlet-basic/WEB-INF/ItemServlet.java +13 -0
  38. package/templates/servlet-basic/WEB-INF/classes/ItemServlet.class +0 -0
  39. package/templates/servlet-flow/WEB-INF/HelloServlet.java +34 -25
  40. package/templates/servlet-flow/WEB-INF/classes/HelloServlet.class +0 -0
  41. package/templates/servlet-jdbc/WEB-INF/JdbcItemServlet.java +13 -0
  42. package/templates/servlet-jdbc/WEB-INF/classes/JdbcItemServlet.class +0 -0
  43. package/templates/snippets/mongo-queries-cheatsheet.txt +105 -0
  44. package/templates/springboot-2model-aggregation-join/.mvn/wrapper/maven-wrapper.properties +3 -0
  45. package/templates/springboot-2model-aggregation-join/mvnw +295 -0
  46. package/templates/springboot-2model-aggregation-join/mvnw.cmd +189 -0
  47. package/templates/springboot-2model-aggregation-join/pom.xml +54 -0
  48. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/AggregationApplication.java +11 -0
  49. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/controller/DepartmentController.java +65 -0
  50. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/controller/EmployeeController.java +65 -0
  51. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/dto/DepartmentStatsDTO.java +15 -0
  52. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/dto/EmployeeJoinDTO.java +19 -0
  53. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/model/Department.java +20 -0
  54. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/model/Employee.java +21 -0
  55. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/repository/DepartmentRepository.java +9 -0
  56. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/repository/EmployeeRepository.java +9 -0
  57. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/service/DepartmentService.java +81 -0
  58. package/templates/springboot-2model-aggregation-join/src/main/java/com/example/aggregation/service/EmployeeService.java +80 -0
  59. package/templates/springboot-2model-aggregation-join/src/main/resources/application.properties +2 -0
  60. package/templates/springboot-2model-aggregation-join/src/test/java/com/example/aggregation/AggregationApplicationTests.java +13 -0
  61. package/templates/vue-2model-crud-patch/README.md +64 -0
  62. package/templates/vue-2model-crud-patch/index.html +12 -0
  63. package/templates/vue-2model-crud-patch/package-lock.json +1785 -0
  64. package/templates/vue-2model-crud-patch/package.json +21 -0
  65. package/templates/vue-2model-crud-patch/src/App.vue +63 -0
  66. package/templates/vue-2model-crud-patch/src/components/AnalyticsBoard.vue +291 -0
  67. package/templates/vue-2model-crud-patch/src/components/DepartmentFormModal.vue +138 -0
  68. package/templates/vue-2model-crud-patch/src/components/DepartmentPatchModal.vue +210 -0
  69. package/templates/vue-2model-crud-patch/src/components/DepartmentTable.vue +133 -0
  70. package/templates/vue-2model-crud-patch/src/components/EmployeeFormModal.vue +177 -0
  71. package/templates/vue-2model-crud-patch/src/components/EmployeePatchModal.vue +221 -0
  72. package/templates/vue-2model-crud-patch/src/components/EmployeeTable.vue +171 -0
  73. package/templates/vue-2model-crud-patch/src/components/NavBar.vue +156 -0
  74. package/templates/vue-2model-crud-patch/src/main.js +12 -0
  75. package/templates/vue-2model-crud-patch/src/router/index.js +33 -0
  76. package/templates/vue-2model-crud-patch/src/services/api.js +64 -0
  77. package/templates/vue-2model-crud-patch/src/stores/departmentStore.js +149 -0
  78. package/templates/vue-2model-crud-patch/src/stores/employeeStore.js +143 -0
  79. package/templates/vue-2model-crud-patch/src/style.css +415 -0
  80. package/templates/vue-2model-crud-patch/src/views/AnalyticsView.vue +59 -0
  81. package/templates/vue-2model-crud-patch/src/views/DepartmentsView.vue +135 -0
  82. package/templates/vue-2model-crud-patch/src/views/EmployeesView.vue +143 -0
  83. package/templates/vue-2model-crud-patch/vite.config.js +9 -0
  84. package/templates/vue-axios/src/App.vue +255 -264
  85. package/templates/vue-axios/src/components/ItemFormModal.vue +183 -0
  86. package/templates/vue-axios/src/components/ItemPatchModal.vue +193 -0
  87. package/templates/vue-axios/src/components/ItemTable.vue +177 -0
  88. package/templates/vue-axios-basic/src/App.vue +241 -429
  89. package/templates/vue-axios-basic/src/components/ItemFormModal.vue +183 -0
  90. package/templates/vue-axios-basic/src/components/ItemPatchModal.vue +193 -0
  91. package/templates/vue-axios-basic/src/components/ItemTable.vue +177 -0
  92. package/templates/vue-axios-pinia/src/App.vue +150 -441
  93. package/templates/vue-axios-pinia/src/components/ItemFormModal.vue +207 -0
  94. package/templates/vue-axios-pinia/src/components/ItemStats.vue +64 -0
  95. package/templates/vue-axios-pinia/src/components/ItemTable.vue +195 -0
  96. package/templates/vue-axios-router/src/App.vue +41 -90
  97. package/templates/vue-axios-router/src/components/ItemFormModal.vue +173 -0
  98. package/templates/vue-axios-router/src/components/ItemTable.vue +102 -0
  99. package/templates/vue-axios-router/src/components/NavBar.vue +57 -0
  100. package/templates/vue-axios-router/src/views/DepartmentView.vue +132 -296
  101. package/templates/vue-axios-router/src/views/EmployeeView.vue +126 -219
  102. package/templates/vue-axios-router-children/src/components/ItemDetail.vue +115 -0
  103. package/templates/vue-axios-router-children/src/components/ItemTable.vue +104 -0
  104. package/templates/vue-axios-router-children/src/components/SideBar.vue +59 -0
  105. package/templates/vue-axios-router-children/src/views/CompanyLayout.vue +2 -51
  106. package/templates/vue-axios-router-children/src/views/DepartmentListView.vue +92 -250
  107. package/templates/vue-axios-router-children/src/views/EmployeeDetailView.vue +59 -181
  108. package/templates/vue-axios-router-children/src/views/EmployeeListView.vue +67 -315
  109. package/templates/vue-axios-router-pinia/src/App.vue +19 -70
  110. package/templates/vue-axios-router-pinia/src/components/ItemFormModal.vue +173 -0
  111. package/templates/vue-axios-router-pinia/src/components/ItemPatchModal.vue +144 -0
  112. package/templates/vue-axios-router-pinia/src/components/ItemTable.vue +102 -0
  113. package/templates/vue-axios-router-pinia/src/components/NavBar.vue +53 -0
  114. package/templates/vue-axios-router-pinia/src/views/DepartmentView.vue +107 -229
  115. package/templates/vue-axios-router-pinia/src/views/EmployeeView.vue +143 -257
  116. package/templates/vue-banking-app/src/components/AccountOverview.vue +75 -0
  117. package/templates/vue-banking-app/src/components/TransactionForm.vue +89 -0
  118. package/templates/vue-banking-app/src/components/TransactionTable.vue +113 -0
  119. package/templates/vue-banking-app/src/stores/bankStore.js +16 -3
  120. package/templates/vue-banking-app/src/views/DashboardView.vue +23 -71
  121. package/templates/vue-banking-app/src/views/StatementView.vue +11 -158
  122. package/templates/vue-banking-app/src/views/TransactionFormView.vue +23 -121
  123. package/templates/vue-basics/src/App.vue +105 -76
  124. package/templates/vue-basics/src/components/CounterCard.vue +107 -0
  125. package/templates/vue-basics/src/components/ItemList.vue +99 -0
  126. package/templates/vue-basics/src/components/UserForm.vue +138 -0
  127. package/templates/vue-cart-router-pinia/src/components/CartSummary.vue +139 -0
  128. package/templates/vue-cart-router-pinia/src/components/CheckoutModal.vue +149 -0
  129. package/templates/vue-cart-router-pinia/src/components/ProductCard.vue +63 -0
  130. package/templates/vue-cart-router-pinia/src/stores/cartStore.js +11 -0
  131. package/templates/vue-cart-router-pinia/src/views/CartView.vue +52 -118
  132. package/templates/vue-cart-router-pinia/src/views/CheckoutView.vue +2 -5
  133. package/templates/vue-cart-router-pinia/src/views/ProductCatalogView.vue +20 -56
  134. package/templates/vue-employee-system/src/App.vue +75 -344
  135. package/templates/vue-employee-system/src/components/DepartmentFilter.vue +108 -0
  136. package/templates/vue-employee-system/src/components/EmployeeModal.vue +183 -0
  137. package/templates/vue-employee-system/src/components/EmployeeTable.vue +91 -0
  138. package/templates/vue-employee-system/src/stores/employeeStore.js +12 -0
  139. package/templates/vue-fetch/src/App.vue +261 -285
  140. package/templates/vue-fetch/src/components/FetchFormModal.vue +183 -0
  141. package/templates/vue-fetch/src/components/FetchPatchModal.vue +193 -0
  142. package/templates/vue-fetch/src/components/FetchTable.vue +177 -0
  143. package/templates/vue-food-ordering/src/components/CartSummary.vue +134 -0
  144. package/templates/vue-food-ordering/src/components/FoodMenu.vue +126 -0
  145. package/templates/vue-food-ordering/src/components/OrderModal.vue +150 -0
  146. package/templates/vue-food-ordering/src/views/MenuView.vue +11 -132
  147. package/templates/vue-food-ordering/src/views/OrderCartView.vue +31 -164
  148. package/templates/vue-game-rps/src/App.vue +29 -88
  149. package/templates/vue-game-tictactoe/src/App.vue +23 -53
  150. package/templates/vue-game-whackamole/src/App.vue +44 -63
  151. package/templates/vue-hospital-portal/src/components/AppointmentForm.vue +144 -0
  152. package/templates/vue-hospital-portal/src/components/DoctorCard.vue +73 -0
  153. package/templates/vue-hospital-portal/src/components/PatientTable.vue +123 -0
  154. package/templates/vue-hospital-portal/src/stores/hospitalStore.js +10 -1
  155. package/templates/vue-hospital-portal/src/views/OPDRegistrationView.vue +40 -416
  156. package/templates/vue-hospital-portal/src/views/PatientListView.vue +11 -301
  157. package/templates/vue-library-system/src/components/BookForm.vue +125 -0
  158. package/templates/vue-library-system/src/components/BookTable.vue +112 -0
  159. package/templates/vue-library-system/src/components/BorrowModal.vue +131 -0
  160. package/templates/vue-library-system/src/stores/libraryStore.js +26 -1
  161. package/templates/vue-library-system/src/views/BookCatalogView.vue +34 -415
  162. package/templates/vue-memory-game/src/App.vue +36 -40
  163. package/templates/vue-memory-game/src/stores/memoryStore.js +32 -37
  164. package/templates/vue-router/src/App.vue +24 -14
  165. package/templates/vue-router/src/components/AboutCard.vue +38 -0
  166. package/templates/vue-router/src/components/HomeHero.vue +75 -0
  167. package/templates/vue-router/src/components/NavBar.vue +54 -0
  168. package/templates/vue-router/src/views/ItemsLayout.vue +17 -9
  169. package/templates/vue-router/src/views/ListView.vue +106 -18
  170. package/templates/vue-router-pinia/src/App.vue +24 -14
  171. package/templates/vue-router-pinia/src/components/ItemFormModal.vue +163 -0
  172. package/templates/vue-router-pinia/src/components/ItemTable.vue +90 -0
  173. package/templates/vue-router-pinia/src/components/NavBar.vue +54 -0
  174. package/templates/vue-router-pinia/src/stores/itemStore.js +8 -1
  175. package/templates/vue-router-pinia/src/views/ListView.vue +78 -14
  176. package/templates/vue-router-pinia-springboot/frontend/src/App.vue +25 -8
  177. package/templates/vue-router-pinia-springboot/frontend/src/components/ItemFormModal.vue +183 -0
  178. package/templates/vue-router-pinia-springboot/frontend/src/components/ItemPatchModal.vue +193 -0
  179. package/templates/vue-router-pinia-springboot/frontend/src/components/ItemTable.vue +177 -0
  180. package/templates/vue-router-pinia-springboot/frontend/src/components/NavBar.vue +54 -0
  181. package/templates/vue-router-pinia-springboot/frontend/src/views/ListView.vue +92 -76
  182. package/templates/vue-springboot-mongo-aggregation/frontend/src/App.vue +184 -193
  183. package/templates/vue-springboot-mongo-aggregation/frontend/src/components/AnalyticsCards.vue +56 -0
  184. package/templates/vue-springboot-mongo-aggregation/frontend/src/components/CategorySummaryTable.vue +71 -0
  185. package/templates/vue-springboot-mongo-aggregation/frontend/src/components/ItemTable.vue +114 -0
  186. package/templates/vue-springboot-mongo-crud/frontend/src/App.vue +228 -158
  187. package/templates/vue-springboot-mongo-crud/frontend/src/components/ItemFormModal.vue +183 -0
  188. package/templates/vue-springboot-mongo-crud/frontend/src/components/ItemPatchModal.vue +193 -0
  189. package/templates/vue-springboot-mongo-crud/frontend/src/components/ItemTable.vue +177 -0
  190. package/templates/vue-symposium-system/src/App.vue +41 -256
  191. package/templates/vue-symposium-system/src/components/AttendeeTable.vue +100 -0
  192. package/templates/vue-symposium-system/src/components/EventCard.vue +67 -0
  193. package/templates/vue-symposium-system/src/components/RegistrationForm.vue +118 -0
  194. package/templates/vue-symposium-system/src/stores/symposiumStore.js +10 -0
  195. package/templates/vue-todo-pinia/src/App.vue +40 -138
  196. package/templates/vue-todo-pinia/src/components/TodoFilter.vue +74 -0
  197. package/templates/vue-todo-pinia/src/components/TodoInput.vue +53 -0
  198. package/templates/vue-todo-pinia/src/components/TodoList.vue +101 -0
  199. package/templates/vue-todo-pinia/src/stores/todoStore.js +6 -0
  200. package/templates/vue-todo-router/src/App.vue +5 -36
  201. package/templates/vue-todo-router/src/components/TodoForm.vue +78 -0
  202. package/templates/vue-todo-router/src/components/TodoList.vue +84 -0
  203. package/templates/vue-todo-router/src/components/TodoNavBar.vue +47 -0
  204. package/templates/vue-todo-router/src/data/todoState.js +7 -0
  205. package/templates/vue-todo-router/src/views/AddTodoView.vue +7 -66
  206. package/templates/vue-todo-router/src/views/TodoDetailView.vue +15 -1
  207. package/templates/vue-todo-router/src/views/TodoListView.vue +22 -64
package/bin/index.js CHANGED
@@ -4,457 +4,407 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
6
 
7
- function copyDirRecursive(src, dest) {
8
- if (typeof fs.cpSync === 'function') {
9
- fs.cpSync(src, dest, { recursive: true });
10
- return;
11
- }
7
+ // ============================================================================
8
+ // WAD LAB TEMPLATES CLI - STRICT ASCII (ZERO EMOJIS)
9
+ // ============================================================================
12
10
 
13
- if (!fs.existsSync(dest)) {
14
- fs.mkdirSync(dest, { recursive: true });
15
- }
16
-
17
- const entries = fs.readdirSync(src, { withFileTypes: true });
18
- for (const entry of entries) {
19
- const srcPath = path.join(src, entry.name);
20
- const destPath = path.join(dest, entry.name);
21
-
22
- if (entry.isDirectory()) {
23
- copyDirRecursive(srcPath, destPath);
24
- } else {
25
- fs.copyFileSync(srcPath, destPath);
26
- }
27
- }
28
- }
29
-
30
- const frontendOptions = [
31
- { name: 'all (Include all 10 Vue variants inside frontend/)', value: 'all' },
32
- { name: 'vue-axios-basic (Vue 3 + Axios Direct CRUD)', value: 'vue-axios-basic' },
33
- { name: 'vue-axios-props-emits (Vue 3 + Axios + Props & Emits)', value: 'vue-axios-props-emits' },
34
- { name: 'vue-axios-router (Vue 3 + Axios + Router Views)', value: 'vue-axios-router' },
35
- { name: 'vue-axios-pinia (Vue 3 + Axios + Pinia Centralized Store)', value: 'vue-axios-pinia' },
36
- { name: 'vue-axios-router-props (Vue 3 + Axios + Router + Props/Emits)', value: 'vue-axios-router-props' },
37
- { name: 'vue-axios-pinia-props (Vue 3 + Axios + Pinia + Props/Emits)', value: 'vue-axios-pinia-props' },
38
- { name: 'vue-axios-router-pinia (Vue 3 + Axios + Router + Pinia)', value: 'vue-axios-router-pinia' },
39
- { name: 'vue-axios-router-pinia-props (The Kitchen Sink — Router + Pinia + Props + Emits)', value: 'vue-axios-router-pinia-props' },
40
- { name: 'vue-axios-router-children (Vue 3 + Axios + Router Nested Children)', value: 'vue-axios-router-children' },
41
- { name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Alerts)', value: 'vue-axios-interceptor' }
42
- ];
43
-
44
- const featuredTemplates = [
11
+ const categories = [
45
12
  {
13
+ id: 'servlet',
46
14
  key: '1',
47
- value: 'fullstack-springboot-vue',
48
- name: '[1] fullstack-springboot-vue (Spring Boot 2-Model API + Selectable Vue Frontend)',
49
- desc: 'Complete stack: 2-model Spring Boot backend + matching Vue frontend (Router/Pinia/Props)'
15
+ title: 'Java Servlets & Tomcat 9',
16
+ desc: '5 Classic Tomcat Servlets, JDBC, Sessions, Voter Eligibility & script.sh'
50
17
  },
51
18
  {
19
+ id: 'springboot',
52
20
  key: '2',
53
- value: 'springboot-2model-1controller-api',
54
- name: '[2] springboot-2model-1controller-api (2 Models Dept & Emp, 2 Repos, 1 Service)',
55
- desc: 'Extensible 2-model Spring Boot architecture (Department + Employee) with full CRUD & PATCH'
21
+ title: 'Spring Boot Backend APIs',
22
+ desc: '10 Pure-Lombok REST APIs: CRUD, Aggregations, Join, 2-Model & MockMvc'
56
23
  },
57
24
  {
25
+ id: 'vue',
58
26
  key: '3',
59
- value: 'springboot-mongo-aggregation-join',
60
- name: '[3] springboot-mongo-aggregation-join ($lookup JOIN, @Query, @Aggregation)',
61
- desc: 'MongoDB $lookup multi-collection JOIN, pipeline stages ($match/$group), and @Query filters'
27
+ title: 'Frontend (Vue 3)',
28
+ desc: '28 Modular Vue 3 Frontends: 2-Model CRUD+PATCH, Axios, Router & Pinia'
62
29
  },
63
30
  {
31
+ id: 'html-css',
64
32
  key: '4',
65
- value: 'vue-axios-router-pinia-props',
66
- name: '[4] vue-axios-router-pinia-props (Matching Frontend: Router + Pinia + Props/Emits)',
67
- desc: 'Matching frontend for 2-model backend: Vue Router + Pinia centralized store + Props/Emits'
33
+ title: 'HTML5 & CSS3 Foundations',
34
+ desc: 'Form Validation, DOM Manipulation & 50 CSS Properties'
68
35
  },
69
36
  {
37
+ id: 'games',
70
38
  key: '5',
71
- value: 'vue-axios-router-props',
72
- name: '[5] vue-axios-router-props (Matching Frontend: Router Views + Props/Emits)',
73
- desc: 'Matching frontend for 2-model backend: Vue Router pages + reusable components with Props/Emits'
74
- },
75
- {
76
- key: '6',
77
- value: 'servlet-jdbc',
78
- name: '[6] servlet-jdbc (Tomcat 9 + MySQL Database Connection via JDBC)',
79
- desc: 'Tomcat Servlet with MySQL Driver, JDBC connection, SQL statement execution, and HTML table'
80
- },
81
- {
82
- key: '7',
83
- value: 'servlet-flow',
84
- name: '[7] servlet-flow (Tomcat 9 Execution Flow with GET/POST Form — No DB)',
85
- desc: 'Pure servlet without DB: HTML form submission, doGet / doPost processing, and response render'
39
+ title: 'Interactive Browser Games',
40
+ desc: '9 Web Games: Bingo, Tic-Tac-Toe, RPS, Whack-a-Mole, Memory'
86
41
  }
87
42
  ];
88
43
 
89
- const categories = [
90
- {
91
- id: 'fullstack',
92
- title: 'Fullstack Web Applications',
93
- desc: 'Integrated Spring Boot + MongoDB + Vue 3 apps'
94
- },
44
+ const templates = [
45
+ // 1. Java Servlets (5)
95
46
  {
96
- id: 'springboot',
97
- title: 'Spring Boot Backend APIs',
98
- desc: 'REST APIs, MongoDB, Aggregations, Join & MockMvc'
47
+ category: 'servlet',
48
+ value: 'servlet-basic',
49
+ name: 'servlet-basic (Basic GET/POST Servlet + Form)',
50
+ desc: 'Classic doGet / doPost request processing with HTML form submission and Tomcat script.sh'
99
51
  },
100
52
  {
101
- id: 'servlet',
102
- title: 'Java Servlets & Tomcat 9',
103
- desc: 'Classic Tomcat Servlets, JDBC, Sessions & Web.xml'
53
+ category: 'servlet',
54
+ value: 'servlet-flow',
55
+ name: 'servlet-flow (RequestDispatcher Forward & Include Flow - No DB)',
56
+ desc: 'Request dispatching between servlets, forward/include execution flow, and Tomcat script.sh'
104
57
  },
105
58
  {
106
- id: 'vue-axios',
107
- title: 'Vue 3 + Axios HTTP Variants',
108
- desc: 'All 10+ architectural combinations of Vue 3 with Axios'
59
+ category: 'servlet',
60
+ value: 'servlet-jdbc',
61
+ name: 'servlet-jdbc (Tomcat 9 + MySQL JDBC Connection & CRUD)',
62
+ desc: 'Servlet with MySQL Driver, JDBC connection, SQL statement execution, HTML table and script.sh'
109
63
  },
110
64
  {
111
- id: 'vue-core',
112
- title: 'Vue 3 Core Portals & State',
113
- desc: 'Router, Pinia, Native Fetch & Complete UI Portals'
65
+ category: 'servlet',
66
+ value: 'servlet-session-tracking',
67
+ name: 'servlet-session-tracking (HttpSession, Cookies, URL Rewriting, Hidden Fields)',
68
+ desc: 'All 4 session tracking mechanisms in Java Servlets with Tomcat script.sh'
114
69
  },
115
70
  {
116
- id: 'games',
117
- title: 'Interactive Games (JS & Vue)',
118
- desc: 'Logic, DOM & State Management Games'
71
+ category: 'servlet',
72
+ value: 'servlet-voter-eligibility',
73
+ name: 'servlet-voter-eligibility (Form Validation & Age Check Logic)',
74
+ desc: 'Server-side input validation, voter eligibility verification, and Tomcat script.sh'
119
75
  },
120
- {
121
- id: 'html-css',
122
- title: 'HTML5 & CSS3 Foundations',
123
- desc: 'Form Validation, DOM Manipulation & 50 CSS Properties'
124
- }
125
- ];
126
76
 
127
- const templates = [
128
- // Fullstack (4)
129
- {
130
- category: 'fullstack',
131
- value: 'fullstack-springboot-vue',
132
- name: 'fullstack-springboot-vue (Spring Boot 2-Model API + Selectable Vue 3 Frontend)'
133
- },
134
- {
135
- category: 'fullstack',
136
- value: 'vue-springboot-mongo-crud',
137
- name: 'vue-springboot-mongo-crud (Spring Boot + MongoDB + Vue 3 Direct CRUD)'
138
- },
77
+ // 2. Spring Boot Backends (10)
139
78
  {
140
- category: 'fullstack',
141
- value: 'vue-springboot-mongo-aggregation',
142
- name: 'vue-springboot-mongo-aggregation (Spring Boot Aggregation + Vue 3 Dashboard)'
79
+ category: 'springboot',
80
+ value: 'springboot-2model-aggregation-join',
81
+ name: 'springboot-2model-aggregation-join (Department + Employee, CRUD + PATCH, $lookup Join, $group Stats)',
82
+ desc: 'Pure Lombok (@Data, @NoArgsConstructor, @AllArgsConstructor), @Autowired injection, CRUD+PATCH, $lookup join & stats'
143
83
  },
144
84
  {
145
- category: 'fullstack',
146
- value: 'vue-router-pinia-springboot',
147
- name: 'vue-router-pinia-springboot (Fullstack Enterprise: Router + Pinia + Spring Boot)'
85
+ category: 'springboot',
86
+ value: 'springboot-2model-1controller-api',
87
+ name: 'springboot-2model-1controller-api (Department + Employee REST API, 2 Repos, 1 Controller)',
88
+ desc: 'Spring Boot 2-model REST API with Department and Employee endpoints in a unified controller'
148
89
  },
149
-
150
- // Spring Boot (9)
151
90
  {
152
91
  category: 'springboot',
153
- value: 'springboot-mongo-crud',
154
- name: 'springboot-mongo-crud (4-Layer REST API: Controller -> Service -> Repo -> Mongo)'
92
+ value: 'springboot-mongo-aggregation-join',
93
+ name: 'springboot-mongo-aggregation-join (Customer + Order, $lookup Join Pipeline)',
94
+ desc: 'Order and Customer MongoDB aggregation join using $lookup, $unwind, and projected DTO'
155
95
  },
156
96
  {
157
97
  category: 'springboot',
158
98
  value: 'springboot-mongo-aggregation',
159
- name: 'springboot-mongo-aggregation (Aggregation Pipeline: $group, $sum, $avg, $sort)'
99
+ name: 'springboot-mongo-aggregation (Standalone Pipeline: $group, $sum, $avg, $sort)',
100
+ desc: 'Single collection aggregation analytics with MongoTemplate and aggregation stages'
160
101
  },
161
102
  {
162
103
  category: 'springboot',
163
- value: 'springboot-mongo-aggregation-join',
164
- name: 'springboot-mongo-aggregation-join (MongoDB $lookup JOIN, @Query, @Aggregation)'
104
+ value: 'springboot-mongo-crud',
105
+ name: 'springboot-mongo-crud (4-Layer REST API: Controller -> Service -> Repo -> Mongo)',
106
+ desc: 'Standard MongoRepository CRUD starter with clean layers and full HTTP methods'
165
107
  },
166
108
  {
167
109
  category: 'springboot',
168
110
  value: 'springboot-mockmvc',
169
- name: 'springboot-mockmvc (Controller Unit Testing with MockMvc)'
111
+ name: 'springboot-mockmvc (Controller Tests with MockMvc, Mockito & AssertJ)',
112
+ desc: 'Unit and integration testing for Spring Boot REST endpoints using MockMvc'
170
113
  },
171
114
  {
172
115
  category: 'springboot',
173
116
  value: 'springboot-mongo-aggregation-mockmvc',
174
- name: 'springboot-mongo-aggregation-mockmvc (Full Testing Lab: MockMvc + Aggregation)'
175
- },
176
- {
177
- category: 'springboot',
178
- value: 'springboot-2model-1controller-api',
179
- name: 'springboot-2model-1controller-api (2 Models Dept & Emp, 2 Repos, 1 Service, 1 Controller)'
117
+ name: 'springboot-mongo-aggregation-mockmvc (Aggregation Controller Covered by MockMvc Tests)',
118
+ desc: 'MongoDB aggregation API with complete test suite covering pipelines and projections'
180
119
  },
181
120
  {
182
121
  category: 'springboot',
183
122
  value: 'springboot-course-registration',
184
- name: 'springboot-course-registration (Exercise 5 — Course Registration REST API)'
123
+ name: 'springboot-course-registration (Student & Course Enrollment REST API)',
124
+ desc: 'Domain REST API for university student registration, courses, and enrollments'
185
125
  },
186
126
  {
187
127
  category: 'springboot',
188
128
  value: 'springboot-hospital-api',
189
- name: 'springboot-hospital-api (Hospital OPD Appointments & Doctors API)'
129
+ name: 'springboot-hospital-api (Doctor & Patient Appointment Scheduling API)',
130
+ desc: 'Domain REST API for hospital appointment booking and patient record management'
190
131
  },
191
132
  {
192
133
  category: 'springboot',
193
134
  value: 'springboot-library-api',
194
- name: 'springboot-library-api (Library Catalog, Loans & Analytics API)'
135
+ name: 'springboot-library-api (Book Catalog & Borrowing Transaction API)',
136
+ desc: 'Domain REST API for book lending, borrowing tracking, and member management'
195
137
  },
196
138
 
197
- // Servlets (5)
139
+ // 3. Frontend (Vue 3) (28)
198
140
  {
199
- category: 'servlet',
200
- value: 'servlet-basic',
201
- name: 'servlet-basic (Tomcat GET + POST Basic Servlet)'
141
+ category: 'vue',
142
+ value: 'vue-2model-crud-patch',
143
+ name: 'vue-2model-crud-patch (2-Model Modular UI: Department & Employee, CRUD + PATCH + Join)',
144
+ desc: 'Vite + Vue 3 + Pinia + Router: Modular components (Tables, FormModals, PatchModals, AnalyticsBoard)'
202
145
  },
203
146
  {
204
- category: 'servlet',
205
- value: 'servlet-flow',
206
- name: 'servlet-flow (HelloServlet Execution Flow with GET/POST Form)'
147
+ category: 'vue',
148
+ value: 'vue-axios-router-pinia-props',
149
+ name: 'vue-axios-router-pinia-props (Full Enterprise: Router + Pinia + Axios + Props/Emits)',
150
+ desc: 'Complete standard stack with multi-view routes, Pinia store, and modular child components'
207
151
  },
208
152
  {
209
- category: 'servlet',
210
- value: 'servlet-jdbc',
211
- name: 'servlet-jdbc (Tomcat + MySQL JDBC Database Connection)'
153
+ category: 'vue',
154
+ value: 'vue-axios-router-props',
155
+ name: 'vue-axios-router-props (Router Navigation + Modular Child Components with Props/Emits)',
156
+ desc: 'Multi-page routing passing state down via props and events up via emits'
212
157
  },
213
158
  {
214
- category: 'servlet',
215
- value: 'servlet-session-tracking',
216
- name: 'servlet-session-tracking (HttpSession, Cookies, Redirect & URL Rewriting)'
159
+ category: 'vue',
160
+ value: 'vue-axios-pinia-props',
161
+ name: 'vue-axios-pinia-props (Pinia Store Passed Down to Child Components via Props)',
162
+ desc: 'Central store feeding child component sections with props and actions'
217
163
  },
218
164
  {
219
- category: 'servlet',
220
- value: 'servlet-voter-eligibility',
221
- name: 'servlet-voter-eligibility (Voting Eligibility Check: Name & Age Validation)'
165
+ category: 'vue',
166
+ value: 'vue-axios-props-emits',
167
+ name: 'vue-axios-props-emits (Component Hierarchy: Form & List with Props Down and Emits Up)',
168
+ desc: 'Clean component isolation communicating purely via props and emit events'
222
169
  },
223
-
224
- // Vue + Axios (11)
225
170
  {
226
- category: 'vue-axios',
171
+ category: 'vue',
227
172
  value: 'vue-axios-basic',
228
- name: 'vue-axios-basic (Vue 3 + Axios Direct CRUD in Single View)'
173
+ name: 'vue-axios-basic (Direct Axios CRUD with Modular Table, Form & Patch Modals)',
174
+ desc: 'Modular Vue 3 component CRUD with Axios HTTP client and partial PATCH modal'
229
175
  },
230
176
  {
231
- category: 'vue-axios-props-emits',
232
- value: 'vue-axios-props-emits',
233
- name: 'vue-axios-props-emits (Vue 3 + Axios + Parent/Child Props & Emits)'
177
+ category: 'vue',
178
+ value: 'vue-axios',
179
+ name: 'vue-axios (Service-Layer Axios CRUD with Modular Components & PATCH)',
180
+ desc: 'ItemService abstraction layer with modular table and patch dialog'
234
181
  },
235
182
  {
236
- category: 'vue-axios-router',
237
- value: 'vue-axios-router',
238
- name: 'vue-axios-router (Vue 3 + Axios + Router Views Dashboard & Details)'
183
+ category: 'vue',
184
+ value: 'vue-axios-interceptor',
185
+ name: 'vue-axios-interceptor (Axios Request/Response Interceptors + Toast Notifications)',
186
+ desc: 'Global HTTP error catching, loading spinners, and toast alerts'
239
187
  },
240
188
  {
241
- category: 'vue-axios-pinia',
189
+ category: 'vue',
242
190
  value: 'vue-axios-pinia',
243
- name: 'vue-axios-pinia (Vue 3 + Axios + Pinia Centralized State Store)'
244
- },
245
- {
246
- category: 'vue-axios-router-props',
247
- value: 'vue-axios-router-props',
248
- name: 'vue-axios-router-props (Vue 3 + Axios + Router + Component Props/Emits)'
249
- },
250
- {
251
- category: 'vue-axios-pinia-props',
252
- value: 'vue-axios-pinia-props',
253
- name: 'vue-axios-pinia-props (Vue 3 + Axios + Pinia Store + Props/Emits)'
191
+ name: 'vue-axios-pinia (Pinia Store with Axios HTTP Actions & Stats)',
192
+ desc: 'Centralized state store managing async CRUD requests with stats component'
254
193
  },
255
194
  {
256
- category: 'vue-axios-router-pinia',
257
- value: 'vue-axios-router-pinia',
258
- name: 'vue-axios-router-pinia (Vue 3 + Axios + Router Views + Pinia Store)'
259
- },
260
- {
261
- category: 'vue-axios-router-pinia-props',
262
- value: 'vue-axios-router-pinia-props',
263
- name: 'vue-axios-router-pinia-props (The Complete Stack: Router + Pinia + Props + Emits)'
195
+ category: 'vue',
196
+ value: 'vue-axios-router',
197
+ name: 'vue-axios-router (Vue Router Multi-View Navigation + Axios API)',
198
+ desc: 'Page views with navigation bar and modular table/form components'
264
199
  },
265
200
  {
266
- category: 'vue-axios-router-children',
201
+ category: 'vue',
267
202
  value: 'vue-axios-router-children',
268
- name: 'vue-axios-router-children (Vue 3 + Axios + Router Nested Children Layouts)'
203
+ name: 'vue-axios-router-children (Nested Routes: Sidebar + Child Detail Views)',
204
+ desc: 'Nested <router-view> routes with sidebar navigation and detail components'
269
205
  },
270
206
  {
271
- category: 'vue-axios-interceptor',
272
- value: 'vue-axios-interceptor',
273
- name: 'vue-axios-interceptor (Vue 3 + Axios Interceptors + Toast Notifications)'
274
- },
275
- {
276
- category: 'vue-axios',
277
- value: 'vue-axios',
278
- name: 'vue-axios (Classic Vue 3 Axios Service Layer CRUD)'
207
+ category: 'vue',
208
+ value: 'vue-axios-router-pinia',
209
+ name: 'vue-axios-router-pinia (Standard SPA: Router + Pinia + Axios Service + PATCH)',
210
+ desc: 'Full SPA architecture with modular tables, modals, and patch actions'
279
211
  },
280
-
281
- // Vue Core & Portals (16)
282
212
  {
283
- category: 'vue-core',
213
+ category: 'vue',
284
214
  value: 'vue-basics',
285
- name: 'vue-basics (Core Reactivity: ref, reactive, computed, v-model, watch)'
215
+ name: 'vue-basics (Template Syntax, v-model, CounterCard, UserForm & ItemList)',
216
+ desc: 'Core Vue 3 reactivity and directives divided into modular components'
286
217
  },
287
218
  {
288
- category: 'vue-props-emits',
219
+ category: 'vue',
289
220
  value: 'vue-props-emits',
290
- name: 'vue-props-emits (Parent/Child Component Communication & Custom Events)'
221
+ name: 'vue-props-emits (Pure Component Communication: ItemCard & ItemForm)',
222
+ desc: 'Parent-child communication without Axios or external libraries'
291
223
  },
292
224
  {
293
- category: 'vue-router',
225
+ category: 'vue',
294
226
  value: 'vue-router',
295
- name: 'vue-router (Vue Router Parent Layout & Nested Children)'
227
+ name: 'vue-router (Vue Router 4 Setup: NavBar, HomeHero & ItemsLayout)',
228
+ desc: 'Clean routing configuration with modular view components'
296
229
  },
297
230
  {
298
- category: 'vue-pinia',
231
+ category: 'vue',
299
232
  value: 'vue-pinia',
300
- name: 'vue-pinia (Pinia State Store: State, Getters, Actions)'
233
+ name: 'vue-pinia (Pinia State Management: ItemForm, ItemList & State Actions)',
234
+ desc: 'Reactive store getters, actions, and state modifications'
301
235
  },
302
236
  {
303
- category: 'vue-router-pinia',
237
+ category: 'vue',
304
238
  value: 'vue-router-pinia',
305
- name: 'vue-router-pinia (Router Multi-Page Views + Pinia Store)'
239
+ name: 'vue-router-pinia (Router + Pinia: NavBar, Store Table & Modal Dialogs)',
240
+ desc: 'Integration of routing and global state without external HTTP'
306
241
  },
307
242
  {
308
- category: 'vue-router-pinia-props',
243
+ category: 'vue',
309
244
  value: 'vue-router-pinia-props',
310
- name: 'vue-router-pinia-props (Router + Pinia Store + Component Props/Emits)'
245
+ name: 'vue-router-pinia-props (Router + Pinia + Child Props/Emits Architecture)',
246
+ desc: 'Hybrid architecture combining global store with prop-driven child components'
311
247
  },
312
248
  {
313
- category: 'vue-fetch',
249
+ category: 'vue',
314
250
  value: 'vue-fetch',
315
- name: 'vue-fetch (Native Fetch API CRUD without Axios)'
251
+ name: 'vue-fetch (Native fetch() API with Modular Table, Form & Patch Modals)',
252
+ desc: 'Native browser fetch without Axios dependency, modular components included'
316
253
  },
317
254
  {
318
- category: 'vue-todo-pinia',
319
- value: 'vue-todo-pinia',
320
- name: 'vue-todo-pinia (Pinia Task & Todo Manager)'
255
+ category: 'vue',
256
+ value: 'vue-banking-app',
257
+ name: 'vue-banking-app (Account Overview, Transaction Form & History Table)',
258
+ desc: 'Banking UI with deposit, withdrawal, transaction ledger, and balance tracking'
321
259
  },
322
260
  {
323
- category: 'vue-todo-router',
324
- value: 'vue-todo-router',
325
- name: 'vue-todo-router (Router Multi-View Task Application)'
261
+ category: 'vue',
262
+ value: 'vue-cart-router-pinia',
263
+ name: 'vue-cart-router-pinia (E-Commerce Cart: Products, Cart Drawer & Badge)',
264
+ desc: 'Shopping cart with product listing, quantity updates, and cart calculations'
326
265
  },
327
266
  {
328
- category: 'vue-cart-router-pinia',
329
- value: 'vue-cart-router-pinia',
330
- name: 'vue-cart-router-pinia (E-Commerce Shopping Cart with Router & Pinia)'
267
+ category: 'vue',
268
+ value: 'vue-employee-system',
269
+ name: 'vue-employee-system (Employee Directory: Department Filter & Modal)',
270
+ desc: 'Staff management with department filtering, payroll calculator, and modals'
331
271
  },
332
272
  {
333
- category: 'vue-food-ordering',
273
+ category: 'vue',
334
274
  value: 'vue-food-ordering',
335
- name: 'vue-food-ordering (Exercise 7 — Online Food Ordering Application)'
275
+ name: 'vue-food-ordering (Restaurant App: Food Menu, Cart Summary & Order Modal)',
276
+ desc: 'Menu catalog with category filters, cart tally, tax computation, and checkout'
277
+ },
278
+ {
279
+ category: 'vue',
280
+ value: 'vue-hospital-portal',
281
+ name: 'vue-hospital-portal (Healthcare Portal: Doctor Cards, Admissions & Patients)',
282
+ desc: 'Patient admission portal with vitals scoring, doctor bookings, and discharge'
336
283
  },
337
284
  {
338
- category: 'vue-symposium-system',
285
+ category: 'vue',
286
+ value: 'vue-library-system',
287
+ name: 'vue-library-system (Library Catalog: Book Table, Inventory Form & Borrow Modal)',
288
+ desc: 'Book catalog with borrowing records, due date tracking, and fine calculation'
289
+ },
290
+ {
291
+ category: 'vue',
339
292
  value: 'vue-symposium-system',
340
- name: 'vue-symposium-system (Exercise 6 — College Symposium Management System)'
293
+ name: 'vue-symposium-system (Event Manager: Event Cards, Registration & Attendees)',
294
+ desc: 'College symposium event registration and attendee roster tracking'
341
295
  },
342
296
  {
343
- category: 'vue-banking-app',
344
- value: 'vue-banking-app',
345
- name: 'vue-banking-app (Banking Portal & Transaction Ledger System)'
297
+ category: 'vue',
298
+ value: 'vue-todo-pinia',
299
+ name: 'vue-todo-pinia (Task Manager: Todo Input, Filter Controls & List)',
300
+ desc: 'Todo tracker with Pinia actions, completion toggles, and badge counters'
346
301
  },
347
302
  {
348
- category: 'vue-employee-system',
349
- value: 'vue-employee-system',
350
- name: 'vue-employee-system (Employee HR, Payroll & Records System)'
303
+ category: 'vue',
304
+ value: 'vue-todo-router',
305
+ name: 'vue-todo-router (Multi-Page Todo: Todo NavBar, Task List & Form View)',
306
+ desc: 'Multi-route todo manager with parameter passing and task creation'
351
307
  },
308
+
309
+ // 4. HTML5 & CSS3 Foundations (2)
352
310
  {
353
- category: 'vue-hospital-portal',
354
- value: 'vue-hospital-portal',
355
- name: 'vue-hospital-portal (Hospital OPD & Patient Vitals Portal)'
311
+ category: 'html-css',
312
+ value: 'html-css-js',
313
+ name: 'html-css-js (HTML5 Semantic Layout, Form Validation & DOM Manipulation)',
314
+ desc: 'Pure vanilla web fundamentals with modern semantic HTML, CSS Flexbox/Grid, and JS validation'
356
315
  },
357
316
  {
358
- category: 'vue-library-system',
359
- value: 'vue-library-system',
360
- name: 'vue-library-system (Library Catalog & Book Circulation System)'
317
+ category: 'html-css',
318
+ value: 'html-css-styles',
319
+ name: 'html-css-styles (50 Core CSS Properties Showcase & Playground)',
320
+ desc: '50 CSS properties reference: Flexbox, CSS Grid, Transitions, Keyframe Animations & Shadows'
361
321
  },
362
322
 
363
- // Games (9)
323
+ // 5. Interactive Browser Games (9)
364
324
  {
365
325
  category: 'games',
366
326
  value: 'game-bingo',
367
- name: 'game-bingo (Exercise 3 — Two-Player JavaScript Bingo Game)'
327
+ name: 'game-bingo (5x5 Randomized Bingo Grid with Win-Line Detection)',
328
+ desc: 'Classic 5x5 Bingo board with random number draw and winning line checks'
368
329
  },
369
330
  {
370
331
  category: 'games',
371
- value: 'game-rock-paper-scissors',
372
- name: 'game-rock-paper-scissors (Player vs Computer Rock Paper Scissors — Vanilla JS)'
332
+ value: 'game-memory-cards',
333
+ name: 'game-memory-cards (Card-Flip Pattern Matching Game in Vanilla JS)',
334
+ desc: 'Memory card matching with flip animations, move count, and match detection'
373
335
  },
374
336
  {
375
337
  category: 'games',
376
- value: 'game-tic-tac-toe',
377
- name: 'game-tic-tac-toe (3x3 Turn Grid with Win Detection — Vanilla JS)'
338
+ value: 'game-rock-paper-scissors',
339
+ name: 'game-rock-paper-scissors (Classic Rock-Paper-Scissors with Score Tracker)',
340
+ desc: 'RPS game against computer AI with streak counting and score history'
378
341
  },
379
342
  {
380
343
  category: 'games',
381
- value: 'game-whack-a-mole',
382
- name: 'game-whack-a-mole (3x3 Mole Whack with Timer & Score — Vanilla JS)'
344
+ value: 'game-tic-tac-toe',
345
+ name: 'game-tic-tac-toe (2-Player Turn-Based Tic-Tac-Toe with Win Logic)',
346
+ desc: 'Classic 3x3 grid with turn toggling, win condition scanning, and reset'
383
347
  },
384
348
  {
385
349
  category: 'games',
386
- value: 'game-memory-cards',
387
- name: 'game-memory-cards (3x4 Flashcard Matching Game — Vanilla JS)'
350
+ value: 'game-whack-a-mole',
351
+ name: 'game-whack-a-mole (Timed Click-Reaction Game with Score Counter)',
352
+ desc: 'Reaction game with randomized mole pops, timer countdown, and high scores'
388
353
  },
389
354
  {
390
355
  category: 'games',
391
356
  value: 'vue-game-rps',
392
- name: 'vue-game-rps (Rock Paper Scissors with Vue 3 + Pinia Scoreboard)'
357
+ name: 'vue-game-rps (Vue 3 Rock-Paper-Scissors with Reactive Scoreboard)',
358
+ desc: 'Vue 3 reactive game with computer AI choice and round history'
393
359
  },
394
360
  {
395
361
  category: 'games',
396
362
  value: 'vue-game-tictactoe',
397
- name: 'vue-game-tictactoe (Tic Tac Toe with Vue 3 + Pinia Move History)'
363
+ name: 'vue-game-tictactoe (Vue 3 Reactive Tic-Tac-Toe with Board State)',
364
+ desc: 'Vue 3 component game with computed winning lines and reset action'
398
365
  },
399
366
  {
400
367
  category: 'games',
401
368
  value: 'vue-game-whackamole',
402
- name: 'vue-game-whackamole (Whack-A-Mole with Vue 3 + Pinia Countdown)'
369
+ name: 'vue-game-whackamole (Vue 3 Whack-a-Mole with Reactive Timer & Score)',
370
+ desc: 'Vue 3 reactive game loop with timer interval and hit detection'
403
371
  },
404
372
  {
405
373
  category: 'games',
406
374
  value: 'vue-memory-game',
407
- name: 'vue-memory-game (Card Matching Game with Vue 3 + Pinia Flips)'
408
- },
409
-
410
- // HTML / CSS (2)
411
- {
412
- category: 'html-css',
413
- value: 'html-css-js',
414
- name: 'html-css-js (Form Validation Regex, Required & Dynamic DOM)'
415
- },
416
- {
417
- category: 'html-css',
418
- value: 'html-css-styles',
419
- name: 'html-css-styles (50 CSS Properties Showcase: Box Model, Flex, Grid, Animations)'
375
+ name: 'vue-memory-game (Vue 3 Card Match Game with Pinia State Store)',
376
+ desc: 'Vue 3 memory card flip game backed by Pinia store actions and timers'
420
377
  }
421
378
  ];
422
379
 
423
- const PAGE_SIZE = 12;
424
380
  const templatesRoot = path.join(__dirname, '..', 'templates');
425
381
 
426
- let currentView = 'MAIN'; // 'MAIN' | 'CATEGORY_LIST' | 'CATEGORY' | 'SEARCH' | 'ALL'
382
+ let currentView = 'MAIN'; // 'MAIN' | 'CATEGORY' | 'SEARCH' | 'ALL'
427
383
  let activeCategory = null;
428
384
  let searchQuery = '';
429
385
  let selectedIndex = 0;
430
386
  const selectedValues = new Set();
431
387
  let isPrompting = false;
432
388
 
389
+ // ============================================================================
390
+ // MULTI-WORD SEARCH & ACTIVE ITEM RETRIEVAL
391
+ // ============================================================================
392
+
433
393
  function getActiveItemList() {
434
394
  if (currentView === 'MAIN') {
435
- const list = featuredTemplates.map(f => ({
436
- type: 'featured',
437
- id: f.value,
438
- name: f.name,
439
- desc: f.desc
440
- }));
441
- list.push({ type: 'action', id: 'categories', name: '[C] Browse by Category (7 Groups, 56 Templates)', desc: 'Explore all 56 templates categorized into 7 groups' });
442
- list.push({ type: 'action', id: 'search', name: '[S] Search by keyword across all 56 templates', desc: 'Type any keyword to find templates instantly' });
443
- list.push({ type: 'action', id: 'all_view', name: '[L] View all 56 templates (grouped catalog)', desc: 'Browse the entire catalog with section dividers' });
444
- list.push({ type: 'action', id: 'unpack_all', name: '[A] Unpack all 56 templates in 1-click', desc: 'Copy every single template into target folder' });
445
- return list;
446
- }
447
-
448
- if (currentView === 'CATEGORY_LIST') {
449
- return categories.map((cat, idx) => {
395
+ const list = categories.map((cat, idx) => {
450
396
  const count = templates.filter(t => t.category === cat.id).length;
451
397
  return {
452
- type: 'cat_entry',
398
+ type: 'category',
453
399
  id: cat.id,
400
+ key: String(idx + 1),
454
401
  name: `[${idx + 1}] ${cat.title} (${count} templates)`,
455
402
  desc: cat.desc
456
403
  };
457
404
  });
405
+ list.push({ type: 'action', id: 'search', key: 'S', name: '[S] Search Templates (Supports multi-word: e.g. "mongo join")', desc: 'Search across all templates with multiple keywords' });
406
+ list.push({ type: 'action', id: 'unpack_all', key: 'A', name: `[A] Unpack All ${templates.length} Templates in 1-Click`, desc: 'Extract the entire catalog into target directory' });
407
+ return list;
458
408
  }
459
409
 
460
410
  if (currentView === 'CATEGORY') {
@@ -464,11 +414,12 @@ function getActiveItemList() {
464
414
  if (currentView === 'SEARCH') {
465
415
  const q = searchQuery.toLowerCase().trim();
466
416
  if (!q) return [];
467
- return templates.filter(t =>
468
- t.value.toLowerCase().includes(q) ||
469
- t.name.toLowerCase().includes(q) ||
470
- t.category.toLowerCase().includes(q)
471
- );
417
+ // Tokenized multi-word search
418
+ const tokens = q.split(/\s+/).filter(Boolean);
419
+ return templates.filter(t => {
420
+ const text = (t.value + ' ' + t.name + ' ' + (t.desc || '') + ' ' + t.category).toLowerCase();
421
+ return tokens.every(token => text.includes(token));
422
+ });
472
423
  }
473
424
 
474
425
  if (currentView === 'ALL') {
@@ -478,28 +429,27 @@ function getActiveItemList() {
478
429
  return [];
479
430
  }
480
431
 
432
+ // ============================================================================
433
+ // TERMINAL SCREEN RENDERING (ZERO EMOJIS)
434
+ // ============================================================================
435
+
481
436
  function renderScreen() {
482
437
  console.clear();
483
438
 
484
439
  if (currentView === 'MAIN') {
485
440
  console.log('========================================================');
486
- console.log(' WAD LAB STARTER TEMPLATES ');
487
- console.log(' Exam Quick-Start & 56 Ready Templates ');
441
+ console.log(' WAD LAB TEMPLATES CLI ');
442
+ console.log(' 54 Clean Starter Templates + Tomcat Deploy ');
488
443
  console.log('========================================================\n');
489
- console.log('CORE EXAM STARTERS:');
490
- console.log(' Press [1-7] for instant pick, or navigate with [UP / DOWN]');
491
- console.log(' [SPACE] Select | [ENTER] Confirm | [C] Categories | [S] Search | [A] All\n');
444
+ console.log('MAIN MENU:');
445
+ console.log(' Press [1-5] to open category, [S] to Search, [A] for All');
446
+ console.log(' Navigate with [UP / DOWN], [ENTER] to confirm\n');
492
447
 
493
448
  const items = getActiveItemList();
494
449
  items.forEach((item, idx) => {
495
- if (item.id === 'categories') {
496
- console.log('\nFULL CATALOG & EXPLORATION:');
497
- }
498
450
  const isCursor = idx === selectedIndex;
499
- const isChecked = selectedValues.has(item.id);
500
- const box = item.type === 'featured' ? (isChecked ? '[x] ' : '[ ] ') : '';
501
451
  const prefix = isCursor ? ' > ' : ' ';
502
- console.log(prefix + box + item.name);
452
+ console.log(prefix + item.name);
503
453
  });
504
454
 
505
455
  const activeItem = items[selectedIndex];
@@ -507,302 +457,219 @@ function renderScreen() {
507
457
  console.log('\n Info: ' + activeItem.desc);
508
458
  }
509
459
  if (selectedValues.size > 0) {
510
- console.log(`\n Selected: ${selectedValues.size} template(s) [Press ENTER to proceed]`);
511
- }
512
- return;
513
- }
514
-
515
- if (currentView === 'CATEGORY_LIST') {
516
- console.log('========================================================');
517
- console.log(' Browse Templates by Category (56 Total)');
518
- console.log('========================================================\n');
519
- console.log(' Press [1-7] or [ENTER] to open category');
520
- console.log(' [B / ESC] Back to Main Menu\n');
521
-
522
- const items = getActiveItemList();
523
- items.forEach((item, idx) => {
524
- const isCursor = idx === selectedIndex;
525
- const prefix = isCursor ? ' > ' : ' ';
526
- console.log(prefix + item.name);
527
- });
528
-
529
- const activeItem = items[selectedIndex];
530
- if (activeItem && activeItem.desc) {
531
- console.log('\n Info: ' + activeItem.desc);
460
+ console.log(`\n Selected (${selectedValues.size}): [${Array.from(selectedValues).join(', ')}]`);
461
+ console.log(' Press [ENTER] to unpack selected, or [X] to clear selection.');
532
462
  }
463
+ console.log('\n [1] Servlets | [2] Spring Boot | [3] Vue 3 | [4] HTML/CSS | [5] Games | [S] Search | [Q] Quit');
533
464
  return;
534
465
  }
535
466
 
536
467
  if (currentView === 'CATEGORY') {
468
+ const items = getActiveItemList();
537
469
  console.log('========================================================');
538
- console.log(` ${activeCategory.title}`);
539
- console.log(` ${activeCategory.desc}`);
470
+ console.log(` ${activeCategory.title} (${items.length} templates)`);
540
471
  console.log('========================================================\n');
541
- console.log(' [SPACE] Select / Deselect (multi-select supported)');
542
- console.log(' [ENTER] Confirm selection and proceed');
543
- console.log(' [B / ESC] Back to Categories Menu\n');
472
+ console.log(' [SPACE] Select | [ENTER] Unpack | [B / ESC] Back to Main Menu\n');
544
473
 
545
- const items = getActiveItemList();
474
+ const PAGE_SIZE = 12;
546
475
  const total = items.length;
547
-
548
476
  let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
549
477
  if (start < 0) start = 0;
550
- if (start + PAGE_SIZE > total) {
551
- start = Math.max(0, total - PAGE_SIZE);
552
- }
553
- const end = Math.min(start + PAGE_SIZE, total);
478
+ if (start + PAGE_SIZE > total) start = Math.max(0, total - PAGE_SIZE);
479
+ const visibleItems = items.slice(start, start + PAGE_SIZE);
554
480
 
555
- if (start > 0) {
556
- console.log(` ^ (${start} more above)`);
557
- }
481
+ if (start > 0) console.log(' ... (scroll up)');
558
482
 
559
- for (let i = start; i < end; i++) {
560
- const t = items[i];
561
- const isCursor = i === selectedIndex;
483
+ visibleItems.forEach((t, i) => {
484
+ const realIndex = start + i;
485
+ const isCursor = realIndex === selectedIndex;
562
486
  const isChecked = selectedValues.has(t.value);
563
- const box = isChecked ? '[x]' : '[ ]';
487
+ const box = isChecked ? '[x] ' : '[ ] ';
564
488
  const prefix = isCursor ? ' > ' : ' ';
565
- console.log(prefix + box + ' ' + t.name);
566
- }
489
+ console.log(prefix + box + `[${realIndex + 1}] ` + t.name);
490
+ });
567
491
 
568
- if (end < total) {
569
- console.log(` v (${total - end} more below)`);
570
- }
492
+ if (start + PAGE_SIZE < total) console.log(' ... (scroll down)');
571
493
 
572
- const selCount = selectedValues.size;
573
- console.log(`\n Selected: ${selCount} template(s) [${selectedIndex + 1}/${total}]`);
494
+ const activeItem = items[selectedIndex];
495
+ if (activeItem && activeItem.desc) {
496
+ console.log('\n Info: ' + activeItem.desc);
497
+ }
498
+ if (selectedValues.size > 0) {
499
+ console.log(`\n Selected: ${selectedValues.size} template(s) [Press ENTER to proceed]`);
500
+ }
574
501
  return;
575
502
  }
576
503
 
577
504
  if (currentView === 'SEARCH') {
578
505
  console.log('========================================================');
579
- console.log(' Instant Template Search');
580
- console.log('========================================================');
581
- console.log(` Query: "${searchQuery}" (type letters/numbers, Backspace to delete)`);
582
- console.log(' [SPACE] Select | [ENTER] Confirm | [ESC] Back\n');
506
+ console.log(' Multi-Word Template Search');
507
+ console.log('========================================================\n');
508
+ console.log(` Query: "${searchQuery}" (Supports multi-word: e.g. "mongo join", "servlet jdbc")`);
509
+ console.log(' [SPACE] Select | [ENTER] Unpack | [ESC] Back to Main Menu\n');
583
510
 
584
511
  const items = getActiveItemList();
585
512
  if (!searchQuery.trim()) {
586
- console.log(' Start typing to search across all 56 templates...');
587
- console.log(' Try keywords: "join", "2model", "props", "servlet", "pinia", "mongo"\n');
513
+ console.log(' Start typing keywords to search across all templates...');
514
+ console.log(' Try queries:');
515
+ console.log(' - "mongo join"');
516
+ console.log(' - "2model"');
517
+ console.log(' - "servlet jdbc"');
518
+ console.log(' - "vue pinia router"');
519
+ console.log(' - "patch"');
520
+ console.log(' - "aggregation"\n');
588
521
  return;
589
522
  }
590
523
 
591
524
  if (items.length === 0) {
592
- console.log(` No templates matched "${searchQuery}".`);
525
+ console.log(` No templates matched all tokens in "${searchQuery}".`);
593
526
  return;
594
527
  }
595
528
 
596
529
  const total = items.length;
530
+ const PAGE_SIZE = 12;
597
531
  let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
598
532
  if (start < 0) start = 0;
599
- if (start + PAGE_SIZE > total) {
600
- start = Math.max(0, total - PAGE_SIZE);
601
- }
602
- const end = Math.min(start + PAGE_SIZE, total);
533
+ if (start + PAGE_SIZE > total) start = Math.max(0, total - PAGE_SIZE);
534
+ const visibleItems = items.slice(start, start + PAGE_SIZE);
603
535
 
604
- if (start > 0) {
605
- console.log(` ^ (${start} more above)`);
606
- }
536
+ if (start > 0) console.log(' ... (scroll up)');
607
537
 
608
- for (let i = start; i < end; i++) {
609
- const t = items[i];
610
- const isCursor = i === selectedIndex;
538
+ visibleItems.forEach((t, i) => {
539
+ const realIndex = start + i;
540
+ const isCursor = realIndex === selectedIndex;
611
541
  const isChecked = selectedValues.has(t.value);
612
- const box = isChecked ? '[x]' : '[ ]';
542
+ const box = isChecked ? '[x] ' : '[ ] ';
613
543
  const prefix = isCursor ? ' > ' : ' ';
614
- console.log(prefix + box + ' ' + t.name);
615
- }
544
+ console.log(prefix + box + t.name + ` [${t.category}]`);
545
+ });
616
546
 
617
- if (end < total) {
618
- console.log(` v (${total - end} more below)`);
619
- }
547
+ if (start + PAGE_SIZE < total) console.log(' ... (scroll down)');
620
548
 
621
- console.log(`\n Matches: ${total} | Selected: ${selectedValues.size}`);
549
+ const activeItem = items[selectedIndex];
550
+ if (activeItem && activeItem.desc) {
551
+ console.log('\n Info: ' + activeItem.desc);
552
+ }
553
+ if (selectedValues.size > 0) {
554
+ console.log(`\n Selected: ${selectedValues.size} template(s) [Press ENTER to proceed]`);
555
+ }
622
556
  return;
623
557
  }
558
+ }
624
559
 
625
- if (currentView === 'ALL') {
626
- console.log('========================================================');
627
- console.log(' Full Catalog: All 56 Templates');
628
- console.log('========================================================\n');
629
- console.log(' [SPACE] Select / Deselect | [ENTER] Confirm | [ESC] Back\n');
560
+ // ============================================================================
561
+ // COPY & EXTRACTION ENGINE
562
+ // ============================================================================
630
563
 
631
- const items = getActiveItemList();
632
- const total = items.length;
564
+ function copyDirectorySync(src, dest) {
565
+ if (!fs.existsSync(dest)) {
566
+ fs.mkdirSync(dest, { recursive: true });
567
+ }
633
568
 
634
- let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
635
- if (start < 0) start = 0;
636
- if (start + PAGE_SIZE > total) {
637
- start = Math.max(0, total - PAGE_SIZE);
638
- }
639
- const end = Math.min(start + PAGE_SIZE, total);
569
+ const entries = fs.readdirSync(src, { withFileTypes: true });
570
+ for (const entry of entries) {
571
+ const srcPath = path.join(src, entry.name);
572
+ const destPath = path.join(dest, entry.name);
640
573
 
641
- if (start > 0) {
642
- console.log(` ^ (${start} more above)`);
574
+ if (entry.name === 'node_modules' || entry.name === 'target' || entry.name === '.git' || entry.name === 'dist') {
575
+ continue;
643
576
  }
644
577
 
645
- let lastCat = null;
646
- for (let i = start; i < end; i++) {
647
- const t = items[i];
648
- if (t.category !== lastCat) {
649
- const catObj = categories.find(c => c.id === t.category);
650
- if (catObj) {
651
- console.log(` --- ${catObj.title} ---`);
652
- }
653
- lastCat = t.category;
578
+ if (entry.isDirectory()) {
579
+ copyDirectorySync(srcPath, destPath);
580
+ } else {
581
+ fs.copyFileSync(srcPath, destPath);
582
+ if (entry.name === 'script.sh' || entry.name === 'mvnw') {
583
+ try { fs.chmodSync(destPath, 0o755); } catch (e) {}
654
584
  }
655
- const isCursor = i === selectedIndex;
656
- const isChecked = selectedValues.has(t.value);
657
- const box = isChecked ? '[x]' : '[ ]';
658
- const prefix = isCursor ? ' > ' : ' ';
659
- console.log(prefix + box + ' ' + t.name);
660
585
  }
586
+ }
587
+ }
588
+
589
+ function unpackTemplates(selected, folderName) {
590
+ const currentDir = process.cwd();
591
+ const targetDir = folderName && folderName !== '.'
592
+ ? path.join(currentDir, folderName)
593
+ : currentDir;
594
+
595
+ console.log('\n--------------------------------------------------------');
596
+ console.log(`Extracting templates into: ${targetDir}`);
597
+ console.log('--------------------------------------------------------\n');
598
+
599
+ let listToUnpack = [];
600
+
601
+ if (selected.includes('all')) {
602
+ const allDirs = fs.readdirSync(templatesRoot).filter(f =>
603
+ fs.statSync(path.join(templatesRoot, f)).isDirectory()
604
+ );
605
+ listToUnpack = allDirs;
606
+ } else {
607
+ listToUnpack = selected;
608
+ }
661
609
 
662
- if (end < total) {
663
- console.log(` v (${total - end} more below)`);
610
+ let count = 0;
611
+ for (const tName of listToUnpack) {
612
+ const sourcePath = path.join(templatesRoot, tName);
613
+ if (!fs.existsSync(sourcePath)) {
614
+ console.log(`[SKIPPED] Template not found: ${tName}`);
615
+ continue;
664
616
  }
665
617
 
666
- console.log(`\n [${selectedIndex + 1}/${total}] Selected: ${selectedValues.size}`);
618
+ let destPath = targetDir;
619
+ if (listToUnpack.length > 1) {
620
+ destPath = path.join(targetDir, tName);
621
+ }
622
+
623
+ try {
624
+ copyDirectorySync(sourcePath, destPath);
625
+ console.log(`[COPIED] ${tName} -> ${destPath}`);
626
+ count++;
627
+ } catch (err) {
628
+ console.error(`[ERROR] Failed to copy ${tName}: ${err.message}`);
629
+ }
667
630
  }
631
+
632
+ console.log('\n========================================================');
633
+ console.log(`SUCCESS: ${count} template(s) unpacked cleanly.`);
634
+ console.log('========================================================');
635
+ console.log('Ready for development.\n');
636
+ process.exit(0);
668
637
  }
669
638
 
670
- function promptFolderName(choices) {
639
+ function promptFolderName(selected) {
671
640
  isPrompting = true;
672
- process.stdin.removeAllListeners('keypress');
673
641
  if (process.stdin.isTTY) {
674
642
  process.stdin.setRawMode(false);
675
643
  }
676
644
 
645
+ const defaultFolder = selected.length === 1 && selected[0] !== 'all' ? selected[0] : '.';
677
646
  const rl = readline.createInterface({
678
647
  input: process.stdin,
679
648
  output: process.stdout
680
649
  });
681
650
 
682
- function askFrontendIfRequired(done) {
683
- if (choices.includes('fullstack-springboot-vue')) {
684
- console.log('\n=== SELECT FRONTEND FOR FULLSTACK TEMPLATE ===');
685
- frontendOptions.forEach((opt, idx) => {
686
- console.log(` [${idx + 1}] ${opt.name}`);
687
- });
688
- rl.question('\nChoose frontend (1-11, default: 1 [all]): ', (ans) => {
689
- const val = ans.trim();
690
- let selectedFrontend = 'all';
691
- const num = parseInt(val, 10);
692
- if (!isNaN(num) && num >= 1 && num <= frontendOptions.length) {
693
- selectedFrontend = frontendOptions[num - 1].value;
694
- } else {
695
- const directMatch = frontendOptions.find(f => f.value === val);
696
- if (directMatch) {
697
- selectedFrontend = directMatch.value;
698
- }
699
- }
700
- done(selectedFrontend);
701
- });
702
- } else {
703
- done(null);
704
- }
705
- }
706
-
707
- askFrontendIfRequired((selectedFrontend) => {
708
- const isAll = choices.includes('all');
709
- const defaultFolder = isAll || choices.length > 1 ? 'wad-templates' : choices[0];
710
-
711
- console.log('\nSelected to unpack: ' + choices.join(', '));
712
- if (selectedFrontend) {
713
- console.log('Selected frontend variant: ' + selectedFrontend);
714
- }
715
-
716
- rl.question(`Enter destination folder name (default: "${defaultFolder}"): `, (answer) => {
717
- rl.close();
718
- const folder = answer.trim() || defaultFolder;
719
- const destDir = path.resolve(process.cwd(), folder);
720
-
721
- if (!fs.existsSync(destDir)) {
722
- fs.mkdirSync(destDir, { recursive: true });
723
- }
724
-
725
- if (isAll) {
726
- copyDirRecursive(templatesRoot, destDir);
727
- console.log('\nSuccessfully unpacked ALL 56 templates into: ' + destDir);
728
- } else {
729
- choices.forEach(c => {
730
- if (c === 'fullstack-springboot-vue') {
731
- const targetFullstack = choices.length === 1 ? destDir : path.join(destDir, c);
732
- if (!fs.existsSync(targetFullstack)) {
733
- fs.mkdirSync(targetFullstack, { recursive: true });
734
- }
735
- const backendSrc = path.join(templatesRoot, 'springboot-2model-1controller-api');
736
- const backendDest = path.join(targetFullstack, 'backend');
737
- copyDirRecursive(backendSrc, backendDest);
738
- console.log('Unpacked [backend] (Spring Boot 2-Model API) into: ' + backendDest);
739
-
740
- const frontendDest = path.join(targetFullstack, 'frontend');
741
- if (selectedFrontend === 'all') {
742
- const fullstackTemplateFrontendSrc = path.join(templatesRoot, 'fullstack-springboot-vue', 'frontend');
743
- copyDirRecursive(fullstackTemplateFrontendSrc, frontendDest);
744
- console.log('Unpacked [frontend] (All 10 Vue variants) into: ' + frontendDest);
745
- } else {
746
- const singleFrontendSrc = path.join(templatesRoot, selectedFrontend);
747
- copyDirRecursive(singleFrontendSrc, frontendDest);
748
- console.log('Unpacked [frontend] (' + selectedFrontend + ') into: ' + frontendDest);
749
- }
750
- } else {
751
- const src = path.join(templatesRoot, c);
752
- const dest = path.join(destDir, c);
753
- copyDirRecursive(src, dest);
754
- console.log('Unpacked [' + c + '] into: ' + dest);
755
- }
756
- });
757
- console.log('\nSuccessfully unpacked all chosen templates into: ' + destDir);
758
- }
759
- process.exit(0);
760
- });
651
+ console.log('\n');
652
+ rl.question(`Enter destination directory (default: "${defaultFolder}"): `, (answer) => {
653
+ rl.close();
654
+ const folder = answer.trim() || defaultFolder;
655
+ unpackTemplates(selected, folder);
761
656
  });
762
657
  }
763
658
 
764
- // Check command-line arguments for direct non-interactive use
765
- if (process.argv[2]) {
766
- const arg = process.argv[2].trim();
767
- if (arg === 'all' || arg === '--all' || arg === '-a') {
768
- promptFolderName(['all']);
769
- } else if (arg === '--cat' && process.argv[3]) {
770
- const catId = process.argv[3].trim().toLowerCase();
771
- const foundCat = categories.find(c => c.id === catId);
772
- if (foundCat) {
773
- activeCategory = foundCat;
774
- currentView = 'CATEGORY';
775
- selectedIndex = 0;
776
- startInteractive();
777
- } else {
778
- console.error(`Unknown category: "${catId}". Available: ${categories.map(c => c.id).join(', ')}`);
779
- process.exit(1);
780
- }
781
- } else if (arg === '--search' && process.argv[3]) {
782
- searchQuery = process.argv.slice(3).join(' ');
783
- currentView = 'SEARCH';
784
- selectedIndex = 0;
785
- startInteractive();
786
- } else {
787
- const found = templates.find(t => t.value === arg);
788
- if (found) {
789
- promptFolderName([found.value]);
790
- } else {
791
- console.error('Unknown template: ' + arg);
792
- console.error(`Available templates (56 total) or run without arguments for the interactive menu.`);
793
- process.exit(1);
794
- }
795
- }
796
- } else {
797
- startInteractive();
798
- }
659
+ // ============================================================================
660
+ // INTERACTIVE INPUT HANDLER
661
+ // ============================================================================
799
662
 
800
663
  function startInteractive() {
801
- readline.emitKeypressEvents(process.stdin);
802
- if (process.stdin.isTTY) {
803
- process.stdin.setRawMode(true);
664
+ if (!process.stdin.isTTY) {
665
+ console.log('Interactive mode requires a TTY terminal. Usage: npx wad-lab [template-name]');
666
+ process.exit(1);
804
667
  }
805
668
 
669
+ readline.emitKeypressEvents(process.stdin);
670
+ process.stdin.setRawMode(true);
671
+ process.stdin.resume();
672
+
806
673
  renderScreen();
807
674
 
808
675
  process.stdin.on('keypress', (str, key) => {
@@ -812,20 +679,18 @@ function startInteractive() {
812
679
  process.exit(0);
813
680
  }
814
681
 
815
- const items = getActiveItemList();
816
-
817
682
  // MAIN VIEW INPUT
818
683
  if (currentView === 'MAIN') {
684
+ const items = getActiveItemList();
685
+
819
686
  if (key.name === 'up') {
820
687
  selectedIndex = (selectedIndex - 1 + items.length) % items.length;
821
688
  renderScreen();
822
689
  } else if (key.name === 'down') {
823
690
  selectedIndex = (selectedIndex + 1) % items.length;
824
691
  renderScreen();
825
- } else if (str === 'c' || str === 'C') {
826
- currentView = 'CATEGORY_LIST';
827
- selectedIndex = 0;
828
- renderScreen();
692
+ } else if (str === 'q' || str === 'Q') {
693
+ process.exit(0);
829
694
  } else if (str === 's' || str === 'S') {
830
695
  currentView = 'SEARCH';
831
696
  searchQuery = '';
@@ -833,36 +698,23 @@ function startInteractive() {
833
698
  renderScreen();
834
699
  } else if (str === 'a' || str === 'A') {
835
700
  promptFolderName(['all']);
836
- } else if (str === 'l' || str === 'L') {
837
- currentView = 'ALL';
838
- selectedIndex = 0;
839
- renderScreen();
840
- } else if (str >= '1' && str <= '7') {
701
+ } else if (str >= '1' && str <= '5') {
841
702
  const num = parseInt(str, 10);
842
- const target = featuredTemplates[num - 1];
843
- if (target) {
844
- promptFolderName([target.value]);
845
- }
846
- } else if (key.name === 'space') {
847
- const item = items[selectedIndex];
848
- if (item.type === 'featured') {
849
- if (selectedValues.has(item.id)) {
850
- selectedValues.delete(item.id);
851
- } else {
852
- selectedValues.add(item.id);
853
- }
703
+ activeCategory = categories[num - 1];
704
+ if (activeCategory) {
705
+ currentView = 'CATEGORY';
706
+ selectedIndex = 0;
854
707
  renderScreen();
855
708
  }
856
709
  } else if (key.name === 'return' || key.name === 'enter') {
857
- const item = items[selectedIndex];
858
710
  if (selectedValues.size > 0) {
859
711
  promptFolderName(Array.from(selectedValues));
860
712
  return;
861
713
  }
862
- if (item.type === 'featured') {
863
- promptFolderName([item.id]);
864
- } else if (item.id === 'categories') {
865
- currentView = 'CATEGORY_LIST';
714
+ const item = items[selectedIndex];
715
+ if (item.type === 'category') {
716
+ activeCategory = categories.find(c => c.id === item.id);
717
+ currentView = 'CATEGORY';
866
718
  selectedIndex = 0;
867
719
  renderScreen();
868
720
  } else if (item.id === 'search') {
@@ -870,19 +722,20 @@ function startInteractive() {
870
722
  searchQuery = '';
871
723
  selectedIndex = 0;
872
724
  renderScreen();
873
- } else if (item.id === 'all_view') {
874
- currentView = 'ALL';
875
- selectedIndex = 0;
876
- renderScreen();
877
725
  } else if (item.id === 'unpack_all') {
878
726
  promptFolderName(['all']);
879
727
  }
728
+ } else if (str === 'x' || str === 'X') {
729
+ selectedValues.clear();
730
+ renderScreen();
880
731
  }
881
732
  return;
882
733
  }
883
734
 
884
- // CATEGORY_LIST VIEW INPUT
885
- if (currentView === 'CATEGORY_LIST') {
735
+ // CATEGORY VIEW INPUT
736
+ if (currentView === 'CATEGORY') {
737
+ const items = getActiveItemList();
738
+
886
739
  if (key.name === 'escape' || key.name === 'left' || str === 'b' || str === 'B') {
887
740
  currentView = 'MAIN';
888
741
  selectedIndex = 0;
@@ -895,23 +748,28 @@ function startInteractive() {
895
748
  } else if (key.name === 'down') {
896
749
  selectedIndex = (selectedIndex + 1) % items.length;
897
750
  renderScreen();
898
- } else if (str >= '1' && str <= '7') {
899
- const num = parseInt(str, 10);
900
- activeCategory = categories[num - 1];
901
- currentView = 'CATEGORY';
902
- selectedIndex = 0;
903
- renderScreen();
751
+ } else if (key.name === 'space') {
752
+ const t = items[selectedIndex];
753
+ if (t) {
754
+ if (selectedValues.has(t.value)) {
755
+ selectedValues.delete(t.value);
756
+ } else {
757
+ selectedValues.add(t.value);
758
+ }
759
+ renderScreen();
760
+ }
904
761
  } else if (key.name === 'return' || key.name === 'enter') {
905
- const item = items[selectedIndex];
906
- activeCategory = categories.find(c => c.id === item.id);
907
- currentView = 'CATEGORY';
908
- selectedIndex = 0;
909
- renderScreen();
762
+ if (selectedValues.size > 0) {
763
+ promptFolderName(Array.from(selectedValues));
764
+ } else {
765
+ const t = items[selectedIndex];
766
+ if (t) promptFolderName([t.value]);
767
+ }
910
768
  }
911
769
  return;
912
770
  }
913
771
 
914
- // SEARCH VIEW INPUT
772
+ // SEARCH VIEW INPUT (MULTI-WORD)
915
773
  if (currentView === 'SEARCH') {
916
774
  if (key.name === 'escape') {
917
775
  currentView = 'MAIN';
@@ -920,43 +778,31 @@ function startInteractive() {
920
778
  return;
921
779
  }
922
780
 
923
- if (key.name === 'up') {
924
- if (items.length > 0) {
925
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
926
- renderScreen();
927
- }
928
- return;
929
- }
781
+ const items = getActiveItemList();
930
782
 
931
- if (key.name === 'down') {
932
- if (items.length > 0) {
933
- selectedIndex = (selectedIndex + 1) % items.length;
934
- renderScreen();
935
- }
783
+ if (key.name === 'up' && items.length > 0) {
784
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
785
+ renderScreen();
936
786
  return;
937
787
  }
938
-
939
- if (key.name === 'space') {
940
- if (items.length > 0) {
941
- const currentVal = items[selectedIndex].value;
942
- if (selectedValues.has(currentVal)) {
943
- selectedValues.delete(currentVal);
944
- } else {
945
- selectedValues.add(currentVal);
946
- }
947
- renderScreen();
948
- }
788
+ if (key.name === 'down' && items.length > 0) {
789
+ selectedIndex = (selectedIndex + 1) % items.length;
790
+ renderScreen();
949
791
  return;
950
792
  }
951
793
 
952
794
  if (key.name === 'return' || key.name === 'enter') {
953
- let chosen = Array.from(selectedValues);
954
- if (chosen.length === 0 && items.length > 0) {
955
- chosen = [items[selectedIndex].value];
795
+ if (selectedValues.size > 0) {
796
+ promptFolderName(Array.from(selectedValues));
797
+ return;
956
798
  }
957
- if (chosen.length > 0) {
958
- promptFolderName(chosen);
799
+ if (items.length > 0) {
800
+ promptFolderName([items[selectedIndex].value]);
801
+ return;
959
802
  }
803
+ }
804
+
805
+ if (key.name === 'space' && key.ctrl) {
960
806
  return;
961
807
  }
962
808
 
@@ -973,63 +819,67 @@ function startInteractive() {
973
819
  return;
974
820
  }
975
821
 
976
- // Typing search query
977
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
822
+ // Typing characters into search (allows spaces for multi-word search!)
823
+ if (str && !key.ctrl && !key.meta) {
978
824
  searchQuery += str;
979
825
  selectedIndex = 0;
980
826
  renderScreen();
981
827
  return;
982
828
  }
983
- return;
984
829
  }
830
+ });
831
+ }
985
832
 
986
- // CATEGORY / ALL VIEW INPUT
987
- if (currentView === 'CATEGORY' || currentView === 'ALL') {
988
- if (key.name === 'escape' || key.name === 'left' || str === 'b' || str === 'B') {
989
- currentView = currentView === 'CATEGORY' ? 'CATEGORY_LIST' : 'MAIN';
990
- selectedIndex = 0;
991
- renderScreen();
992
- return;
993
- }
833
+ // ============================================================================
834
+ // CLI ARGUMENT DISPATCHER
835
+ // ============================================================================
994
836
 
995
- if (key.name === 'up') {
996
- if (items.length > 0) {
997
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
998
- renderScreen();
999
- }
1000
- return;
1001
- }
837
+ function runCli() {
838
+ const arg = process.argv[2];
1002
839
 
1003
- if (key.name === 'down') {
1004
- if (items.length > 0) {
1005
- selectedIndex = (selectedIndex + 1) % items.length;
1006
- renderScreen();
1007
- }
1008
- return;
1009
- }
840
+ if (!arg) {
841
+ startInteractive();
842
+ return;
843
+ }
1010
844
 
1011
- if (key.name === 'space') {
1012
- if (items.length > 0) {
1013
- const currentVal = items[selectedIndex].value;
1014
- if (selectedValues.has(currentVal)) {
1015
- selectedValues.delete(currentVal);
1016
- } else {
1017
- selectedValues.add(currentVal);
1018
- }
1019
- renderScreen();
1020
- }
1021
- return;
1022
- }
845
+ if (arg === '--help' || arg === '-h') {
846
+ console.log('WAD Lab Templates CLI (Zero Emojis)');
847
+ console.log('Usage:');
848
+ console.log(' npx wad-lab Launch interactive UI');
849
+ console.log(' npx wad-lab [template-name] Directly unpack a template');
850
+ console.log(' npx wad-lab --list List all available templates');
851
+ console.log(' npx wad-lab --search [words...] Search templates with multi-word filter');
852
+ process.exit(0);
853
+ }
1023
854
 
1024
- if (key.name === 'return' || key.name === 'enter') {
1025
- let chosen = Array.from(selectedValues);
1026
- if (chosen.length === 0 && items.length > 0) {
1027
- chosen = [items[selectedIndex].value];
1028
- }
1029
- if (chosen.length > 0) {
1030
- promptFolderName(chosen);
1031
- }
1032
- }
1033
- }
1034
- });
855
+ if (arg === '--list' || arg === '-l') {
856
+ console.log('Available WAD Lab Templates (54 Standalone):');
857
+ categories.forEach(cat => {
858
+ console.log(`\n=== ${cat.title} ===`);
859
+ templates.filter(t => t.category === cat.id).forEach(t => {
860
+ console.log(` - ${t.value.padEnd(38)} : ${t.desc}`);
861
+ });
862
+ });
863
+ process.exit(0);
864
+ }
865
+
866
+ if (arg === '--search' && process.argv[3]) {
867
+ searchQuery = process.argv.slice(3).join(' ');
868
+ currentView = 'SEARCH';
869
+ selectedIndex = 0;
870
+ startInteractive();
871
+ return;
872
+ }
873
+
874
+ const found = templates.find(t => t.value === arg);
875
+ if (found) {
876
+ const dest = process.argv[3] || found.value;
877
+ unpackTemplates([found.value], dest);
878
+ } else {
879
+ console.log(`Unknown template or command: "${arg}".`);
880
+ console.log('Run "npx wad-lab --list" to view all available templates.');
881
+ process.exit(1);
882
+ }
1035
883
  }
884
+
885
+ runCli();