wad-lab 1.2.2 → 1.2.3

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 +472 -631
  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)'
191
+ name: 'vue-axios-pinia (Pinia Store with Axios HTTP Actions & Stats)',
192
+ desc: 'Centralized state store managing async CRUD requests with stats component'
244
193
  },
245
194
  {
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)'
254
- },
255
- {
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'
283
+ },
284
+ {
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'
336
289
  },
337
290
  {
338
- category: 'vue-symposium-system',
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 58 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,46 +429,21 @@ 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');
492
-
493
- const items = getActiveItemList();
494
- items.forEach((item, idx) => {
495
- if (item.id === 'categories') {
496
- console.log('\nFULL CATALOG & EXPLORATION:');
497
- }
498
- const isCursor = idx === selectedIndex;
499
- const isChecked = selectedValues.has(item.id);
500
- const box = item.type === 'featured' ? (isChecked ? '[x] ' : '[ ] ') : '';
501
- const prefix = isCursor ? ' > ' : ' ';
502
- console.log(prefix + box + item.name);
503
- });
504
-
505
- const activeItem = items[selectedIndex];
506
- if (activeItem && activeItem.desc) {
507
- console.log('\n Info: ' + activeItem.desc);
508
- }
509
- 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');
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');
521
447
 
522
448
  const items = getActiveItemList();
523
449
  items.forEach((item, idx) => {
@@ -530,279 +456,216 @@ function renderScreen() {
530
456
  if (activeItem && activeItem.desc) {
531
457
  console.log('\n Info: ' + activeItem.desc);
532
458
  }
459
+ console.log('\n [1] Servlets | [2] Spring Boot | [3] Vue 3 | [4] HTML/CSS | [5] Games | [S] Search | [Q] Quit');
533
460
  return;
534
461
  }
535
462
 
536
463
  if (currentView === 'CATEGORY') {
464
+ const items = getActiveItemList();
537
465
  console.log('========================================================');
538
- console.log(` ${activeCategory.title}`);
539
- console.log(` ${activeCategory.desc}`);
466
+ console.log(` ${activeCategory.title} (${items.length} templates)`);
540
467
  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');
468
+ console.log(' [SPACE] Select | [ENTER] Unpack | [B / ESC] Back to Main Menu\n');
544
469
 
545
- const items = getActiveItemList();
470
+ const PAGE_SIZE = 12;
546
471
  const total = items.length;
547
-
548
472
  let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
549
473
  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);
474
+ if (start + PAGE_SIZE > total) start = Math.max(0, total - PAGE_SIZE);
475
+ const visibleItems = items.slice(start, start + PAGE_SIZE);
554
476
 
555
- if (start > 0) {
556
- console.log(` ^ (${start} more above)`);
557
- }
477
+ if (start > 0) console.log(' ... (scroll up)');
558
478
 
559
- for (let i = start; i < end; i++) {
560
- const t = items[i];
561
- const isCursor = i === selectedIndex;
479
+ visibleItems.forEach((t, i) => {
480
+ const realIndex = start + i;
481
+ const isCursor = realIndex === selectedIndex;
562
482
  const isChecked = selectedValues.has(t.value);
563
- const box = isChecked ? '[x]' : '[ ]';
483
+ const box = isChecked ? '[x] ' : '[ ] ';
564
484
  const prefix = isCursor ? ' > ' : ' ';
565
- console.log(prefix + box + ' ' + t.name);
566
- }
485
+ console.log(prefix + box + `[${realIndex + 1}] ` + t.name);
486
+ });
567
487
 
568
- if (end < total) {
569
- console.log(` v (${total - end} more below)`);
570
- }
488
+ if (start + PAGE_SIZE < total) console.log(' ... (scroll down)');
571
489
 
572
- const selCount = selectedValues.size;
573
- console.log(`\n Selected: ${selCount} template(s) [${selectedIndex + 1}/${total}]`);
490
+ const activeItem = items[selectedIndex];
491
+ if (activeItem && activeItem.desc) {
492
+ console.log('\n Info: ' + activeItem.desc);
493
+ }
494
+ if (selectedValues.size > 0) {
495
+ console.log(`\n Selected: ${selectedValues.size} template(s) [Press ENTER to proceed]`);
496
+ }
574
497
  return;
575
498
  }
576
499
 
577
500
  if (currentView === 'SEARCH') {
578
501
  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');
502
+ console.log(' Multi-Word Template Search');
503
+ console.log('========================================================\n');
504
+ console.log(` Query: "${searchQuery}" (Supports multi-word: e.g. "mongo join", "servlet jdbc")`);
505
+ console.log(' [SPACE] Select | [ENTER] Unpack | [ESC] Back to Main Menu\n');
583
506
 
584
507
  const items = getActiveItemList();
585
508
  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');
509
+ console.log(' Start typing keywords to search across all templates...');
510
+ console.log(' Try queries:');
511
+ console.log(' - "mongo join"');
512
+ console.log(' - "2model"');
513
+ console.log(' - "servlet jdbc"');
514
+ console.log(' - "vue pinia router"');
515
+ console.log(' - "patch"');
516
+ console.log(' - "aggregation"\n');
588
517
  return;
589
518
  }
590
519
 
591
520
  if (items.length === 0) {
592
- console.log(` No templates matched "${searchQuery}".`);
521
+ console.log(` No templates matched all tokens in "${searchQuery}".`);
593
522
  return;
594
523
  }
595
524
 
596
525
  const total = items.length;
526
+ const PAGE_SIZE = 12;
597
527
  let start = selectedIndex - Math.floor(PAGE_SIZE / 2);
598
528
  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);
529
+ if (start + PAGE_SIZE > total) start = Math.max(0, total - PAGE_SIZE);
530
+ const visibleItems = items.slice(start, start + PAGE_SIZE);
603
531
 
604
- if (start > 0) {
605
- console.log(` ^ (${start} more above)`);
606
- }
532
+ if (start > 0) console.log(' ... (scroll up)');
607
533
 
608
- for (let i = start; i < end; i++) {
609
- const t = items[i];
610
- const isCursor = i === selectedIndex;
534
+ visibleItems.forEach((t, i) => {
535
+ const realIndex = start + i;
536
+ const isCursor = realIndex === selectedIndex;
611
537
  const isChecked = selectedValues.has(t.value);
612
- const box = isChecked ? '[x]' : '[ ]';
538
+ const box = isChecked ? '[x] ' : '[ ] ';
613
539
  const prefix = isCursor ? ' > ' : ' ';
614
- console.log(prefix + box + ' ' + t.name);
615
- }
540
+ console.log(prefix + box + t.name + ` [${t.category}]`);
541
+ });
616
542
 
617
- if (end < total) {
618
- console.log(` v (${total - end} more below)`);
619
- }
543
+ if (start + PAGE_SIZE < total) console.log(' ... (scroll down)');
620
544
 
621
- console.log(`\n Matches: ${total} | Selected: ${selectedValues.size}`);
545
+ const activeItem = items[selectedIndex];
546
+ if (activeItem && activeItem.desc) {
547
+ console.log('\n Info: ' + activeItem.desc);
548
+ }
549
+ if (selectedValues.size > 0) {
550
+ console.log(`\n Selected: ${selectedValues.size} template(s) [Press ENTER to proceed]`);
551
+ }
622
552
  return;
623
553
  }
554
+ }
624
555
 
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');
556
+ // ============================================================================
557
+ // COPY & EXTRACTION ENGINE
558
+ // ============================================================================
630
559
 
631
- const items = getActiveItemList();
632
- const total = items.length;
560
+ function copyDirectorySync(src, dest) {
561
+ if (!fs.existsSync(dest)) {
562
+ fs.mkdirSync(dest, { recursive: true });
563
+ }
633
564
 
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);
565
+ const entries = fs.readdirSync(src, { withFileTypes: true });
566
+ for (const entry of entries) {
567
+ const srcPath = path.join(src, entry.name);
568
+ const destPath = path.join(dest, entry.name);
640
569
 
641
- if (start > 0) {
642
- console.log(` ^ (${start} more above)`);
570
+ if (entry.name === 'node_modules' || entry.name === 'target' || entry.name === '.git' || entry.name === 'dist') {
571
+ continue;
643
572
  }
644
573
 
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;
574
+ if (entry.isDirectory()) {
575
+ copyDirectorySync(srcPath, destPath);
576
+ } else {
577
+ fs.copyFileSync(srcPath, destPath);
578
+ if (entry.name === 'script.sh' || entry.name === 'mvnw') {
579
+ try { fs.chmodSync(destPath, 0o755); } catch (e) {}
654
580
  }
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);
581
+ }
582
+ }
583
+ }
584
+
585
+ function unpackTemplates(selected, folderName) {
586
+ const currentDir = process.cwd();
587
+ const targetDir = folderName && folderName !== '.'
588
+ ? path.join(currentDir, folderName)
589
+ : currentDir;
590
+
591
+ console.log('\n--------------------------------------------------------');
592
+ console.log(`Extracting templates into: ${targetDir}`);
593
+ console.log('--------------------------------------------------------\n');
594
+
595
+ let listToUnpack = [];
596
+
597
+ if (selected.includes('all')) {
598
+ const allDirs = fs.readdirSync(templatesRoot).filter(f =>
599
+ fs.statSync(path.join(templatesRoot, f)).isDirectory()
600
+ );
601
+ listToUnpack = allDirs;
602
+ } else {
603
+ listToUnpack = selected;
604
+ }
605
+
606
+ let count = 0;
607
+ for (const tName of listToUnpack) {
608
+ const sourcePath = path.join(templatesRoot, tName);
609
+ if (!fs.existsSync(sourcePath)) {
610
+ console.log(`[SKIPPED] Template not found: ${tName}`);
611
+ continue;
660
612
  }
661
613
 
662
- if (end < total) {
663
- console.log(` v (${total - end} more below)`);
614
+ let destPath = targetDir;
615
+ if (listToUnpack.length > 1) {
616
+ destPath = path.join(targetDir, tName);
664
617
  }
665
618
 
666
- console.log(`\n [${selectedIndex + 1}/${total}] Selected: ${selectedValues.size}`);
619
+ try {
620
+ copyDirectorySync(sourcePath, destPath);
621
+ console.log(`[COPIED] ${tName} -> ${destPath}`);
622
+ count++;
623
+ } catch (err) {
624
+ console.error(`[ERROR] Failed to copy ${tName}: ${err.message}`);
625
+ }
667
626
  }
627
+
628
+ console.log('\n========================================================');
629
+ console.log(`SUCCESS: ${count} template(s) unpacked cleanly.`);
630
+ console.log('========================================================');
631
+ console.log('Ready for development.\n');
632
+ process.exit(0);
668
633
  }
669
634
 
670
- function promptFolderName(choices) {
635
+ function promptFolderName(selected) {
671
636
  isPrompting = true;
672
- process.stdin.removeAllListeners('keypress');
673
637
  if (process.stdin.isTTY) {
674
638
  process.stdin.setRawMode(false);
675
639
  }
676
640
 
641
+ const defaultFolder = selected.length === 1 && selected[0] !== 'all' ? selected[0] : '.';
677
642
  const rl = readline.createInterface({
678
643
  input: process.stdin,
679
644
  output: process.stdout
680
645
  });
681
646
 
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
- });
647
+ console.log('\n');
648
+ rl.question(`Enter destination directory (default: "${defaultFolder}"): `, (answer) => {
649
+ rl.close();
650
+ const folder = answer.trim() || defaultFolder;
651
+ unpackTemplates(selected, folder);
761
652
  });
762
653
  }
763
654
 
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
- }
655
+ // ============================================================================
656
+ // INTERACTIVE INPUT HANDLER
657
+ // ============================================================================
799
658
 
800
659
  function startInteractive() {
801
- readline.emitKeypressEvents(process.stdin);
802
- if (process.stdin.isTTY) {
803
- process.stdin.setRawMode(true);
660
+ if (!process.stdin.isTTY) {
661
+ console.log('Interactive mode requires a TTY terminal. Usage: npx wad-lab [template-name]');
662
+ process.exit(1);
804
663
  }
805
664
 
665
+ readline.emitKeypressEvents(process.stdin);
666
+ process.stdin.setRawMode(true);
667
+ process.stdin.resume();
668
+
806
669
  renderScreen();
807
670
 
808
671
  process.stdin.on('keypress', (str, key) => {
@@ -812,20 +675,18 @@ function startInteractive() {
812
675
  process.exit(0);
813
676
  }
814
677
 
815
- const items = getActiveItemList();
816
-
817
678
  // MAIN VIEW INPUT
818
679
  if (currentView === 'MAIN') {
680
+ const items = getActiveItemList();
681
+
819
682
  if (key.name === 'up') {
820
683
  selectedIndex = (selectedIndex - 1 + items.length) % items.length;
821
684
  renderScreen();
822
685
  } else if (key.name === 'down') {
823
686
  selectedIndex = (selectedIndex + 1) % items.length;
824
687
  renderScreen();
825
- } else if (str === 'c' || str === 'C') {
826
- currentView = 'CATEGORY_LIST';
827
- selectedIndex = 0;
828
- renderScreen();
688
+ } else if (str === 'q' || str === 'Q') {
689
+ process.exit(0);
829
690
  } else if (str === 's' || str === 'S') {
830
691
  currentView = 'SEARCH';
831
692
  searchQuery = '';
@@ -833,47 +694,28 @@ function startInteractive() {
833
694
  renderScreen();
834
695
  } else if (str === 'a' || str === 'A') {
835
696
  promptFolderName(['all']);
836
- } else if (str === 'l' || str === 'L') {
837
- currentView = 'ALL';
838
- selectedIndex = 0;
839
- renderScreen();
840
- } else if (str >= '1' && str <= '7') {
697
+ } else if (str >= '1' && str <= '5') {
841
698
  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
- }
699
+ activeCategory = categories[num - 1];
700
+ if (activeCategory) {
701
+ currentView = 'CATEGORY';
702
+ selectedIndex = 0;
703
+ selectedValues.clear();
854
704
  renderScreen();
855
705
  }
856
706
  } else if (key.name === 'return' || key.name === 'enter') {
857
707
  const item = items[selectedIndex];
858
- if (selectedValues.size > 0) {
859
- promptFolderName(Array.from(selectedValues));
860
- return;
861
- }
862
- if (item.type === 'featured') {
863
- promptFolderName([item.id]);
864
- } else if (item.id === 'categories') {
865
- currentView = 'CATEGORY_LIST';
708
+ if (item.type === 'category') {
709
+ activeCategory = categories.find(c => c.id === item.id);
710
+ currentView = 'CATEGORY';
866
711
  selectedIndex = 0;
712
+ selectedValues.clear();
867
713
  renderScreen();
868
714
  } else if (item.id === 'search') {
869
715
  currentView = 'SEARCH';
870
716
  searchQuery = '';
871
717
  selectedIndex = 0;
872
718
  renderScreen();
873
- } else if (item.id === 'all_view') {
874
- currentView = 'ALL';
875
- selectedIndex = 0;
876
- renderScreen();
877
719
  } else if (item.id === 'unpack_all') {
878
720
  promptFolderName(['all']);
879
721
  }
@@ -881,8 +723,10 @@ function startInteractive() {
881
723
  return;
882
724
  }
883
725
 
884
- // CATEGORY_LIST VIEW INPUT
885
- if (currentView === 'CATEGORY_LIST') {
726
+ // CATEGORY VIEW INPUT
727
+ if (currentView === 'CATEGORY') {
728
+ const items = getActiveItemList();
729
+
886
730
  if (key.name === 'escape' || key.name === 'left' || str === 'b' || str === 'B') {
887
731
  currentView = 'MAIN';
888
732
  selectedIndex = 0;
@@ -895,23 +739,28 @@ function startInteractive() {
895
739
  } else if (key.name === 'down') {
896
740
  selectedIndex = (selectedIndex + 1) % items.length;
897
741
  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();
742
+ } else if (key.name === 'space') {
743
+ const t = items[selectedIndex];
744
+ if (t) {
745
+ if (selectedValues.has(t.value)) {
746
+ selectedValues.delete(t.value);
747
+ } else {
748
+ selectedValues.add(t.value);
749
+ }
750
+ renderScreen();
751
+ }
904
752
  } 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();
753
+ if (selectedValues.size > 0) {
754
+ promptFolderName(Array.from(selectedValues));
755
+ } else {
756
+ const t = items[selectedIndex];
757
+ if (t) promptFolderName([t.value]);
758
+ }
910
759
  }
911
760
  return;
912
761
  }
913
762
 
914
- // SEARCH VIEW INPUT
763
+ // SEARCH VIEW INPUT (MULTI-WORD)
915
764
  if (currentView === 'SEARCH') {
916
765
  if (key.name === 'escape') {
917
766
  currentView = 'MAIN';
@@ -920,43 +769,31 @@ function startInteractive() {
920
769
  return;
921
770
  }
922
771
 
923
- if (key.name === 'up') {
924
- if (items.length > 0) {
925
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
926
- renderScreen();
927
- }
928
- return;
929
- }
772
+ const items = getActiveItemList();
930
773
 
931
- if (key.name === 'down') {
932
- if (items.length > 0) {
933
- selectedIndex = (selectedIndex + 1) % items.length;
934
- renderScreen();
935
- }
774
+ if (key.name === 'up' && items.length > 0) {
775
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
776
+ renderScreen();
936
777
  return;
937
778
  }
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
- }
779
+ if (key.name === 'down' && items.length > 0) {
780
+ selectedIndex = (selectedIndex + 1) % items.length;
781
+ renderScreen();
949
782
  return;
950
783
  }
951
784
 
952
785
  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];
786
+ if (selectedValues.size > 0) {
787
+ promptFolderName(Array.from(selectedValues));
788
+ return;
956
789
  }
957
- if (chosen.length > 0) {
958
- promptFolderName(chosen);
790
+ if (items.length > 0) {
791
+ promptFolderName([items[selectedIndex].value]);
792
+ return;
959
793
  }
794
+ }
795
+
796
+ if (key.name === 'space' && key.ctrl) {
960
797
  return;
961
798
  }
962
799
 
@@ -973,63 +810,67 @@ function startInteractive() {
973
810
  return;
974
811
  }
975
812
 
976
- // Typing search query
977
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
813
+ // Typing characters into search (allows spaces for multi-word search!)
814
+ if (str && !key.ctrl && !key.meta) {
978
815
  searchQuery += str;
979
816
  selectedIndex = 0;
980
817
  renderScreen();
981
818
  return;
982
819
  }
983
- return;
984
820
  }
821
+ });
822
+ }
985
823
 
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
- }
824
+ // ============================================================================
825
+ // CLI ARGUMENT DISPATCHER
826
+ // ============================================================================
994
827
 
995
- if (key.name === 'up') {
996
- if (items.length > 0) {
997
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
998
- renderScreen();
999
- }
1000
- return;
1001
- }
828
+ function runCli() {
829
+ const arg = process.argv[2];
1002
830
 
1003
- if (key.name === 'down') {
1004
- if (items.length > 0) {
1005
- selectedIndex = (selectedIndex + 1) % items.length;
1006
- renderScreen();
1007
- }
1008
- return;
1009
- }
831
+ if (!arg) {
832
+ startInteractive();
833
+ return;
834
+ }
1010
835
 
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
- }
836
+ if (arg === '--help' || arg === '-h') {
837
+ console.log('WAD Lab Templates CLI (Zero Emojis)');
838
+ console.log('Usage:');
839
+ console.log(' npx wad-lab Launch interactive UI');
840
+ console.log(' npx wad-lab [template-name] Directly unpack a template');
841
+ console.log(' npx wad-lab --list List all available templates');
842
+ console.log(' npx wad-lab --search [words...] Search templates with multi-word filter');
843
+ process.exit(0);
844
+ }
1023
845
 
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
- });
846
+ if (arg === '--list' || arg === '-l') {
847
+ console.log('Available WAD Lab Templates (54 Standalone):');
848
+ categories.forEach(cat => {
849
+ console.log(`\n=== ${cat.title} ===`);
850
+ templates.filter(t => t.category === cat.id).forEach(t => {
851
+ console.log(` - ${t.value.padEnd(38)} : ${t.desc}`);
852
+ });
853
+ });
854
+ process.exit(0);
855
+ }
856
+
857
+ if (arg === '--search' && process.argv[3]) {
858
+ searchQuery = process.argv.slice(3).join(' ');
859
+ currentView = 'SEARCH';
860
+ selectedIndex = 0;
861
+ startInteractive();
862
+ return;
863
+ }
864
+
865
+ const found = templates.find(t => t.value === arg);
866
+ if (found) {
867
+ const dest = process.argv[3] || found.value;
868
+ unpackTemplates([found.value], dest);
869
+ } else {
870
+ console.log(`Unknown template or command: "${arg}".`);
871
+ console.log('Run "npx wad-lab --list" to view all available templates.');
872
+ process.exit(1);
873
+ }
1035
874
  }
875
+
876
+ runCli();