@intranefr/superbackend 1.4.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 (188) hide show
  1. package/.commiat +4 -0
  2. package/.env.example +47 -0
  3. package/README.md +110 -0
  4. package/index.js +94 -0
  5. package/package.json +67 -0
  6. package/public/css/styles.css +139 -0
  7. package/public/js/animations.js +41 -0
  8. package/sdk/error-tracking/browser/package.json +16 -0
  9. package/sdk/error-tracking/browser/src/core.js +270 -0
  10. package/sdk/error-tracking/browser/src/embed.js +18 -0
  11. package/sdk/error-tracking/browser/src/index.js +1 -0
  12. package/server.js +5 -0
  13. package/src/admin/endpointRegistry.js +300 -0
  14. package/src/controllers/admin.controller.js +321 -0
  15. package/src/controllers/adminAssets.controller.js +530 -0
  16. package/src/controllers/adminAssetsStorage.controller.js +260 -0
  17. package/src/controllers/adminEjsVirtual.controller.js +354 -0
  18. package/src/controllers/adminFeatureFlags.controller.js +155 -0
  19. package/src/controllers/adminHeadless.controller.js +1071 -0
  20. package/src/controllers/adminI18n.controller.js +604 -0
  21. package/src/controllers/adminJsonConfigs.controller.js +97 -0
  22. package/src/controllers/adminLlm.controller.js +273 -0
  23. package/src/controllers/adminMigration.controller.js +257 -0
  24. package/src/controllers/adminSeoConfig.controller.js +515 -0
  25. package/src/controllers/adminStats.controller.js +121 -0
  26. package/src/controllers/adminUploadNamespaces.controller.js +208 -0
  27. package/src/controllers/assets.controller.js +248 -0
  28. package/src/controllers/auth.controller.js +93 -0
  29. package/src/controllers/billing.controller.js +223 -0
  30. package/src/controllers/featureFlags.controller.js +35 -0
  31. package/src/controllers/forms.controller.js +217 -0
  32. package/src/controllers/globalSettings.controller.js +252 -0
  33. package/src/controllers/headlessCrud.controller.js +126 -0
  34. package/src/controllers/i18n.controller.js +12 -0
  35. package/src/controllers/invite.controller.js +249 -0
  36. package/src/controllers/jsonConfigs.controller.js +19 -0
  37. package/src/controllers/metrics.controller.js +149 -0
  38. package/src/controllers/notificationAdmin.controller.js +264 -0
  39. package/src/controllers/notifications.controller.js +131 -0
  40. package/src/controllers/org.controller.js +357 -0
  41. package/src/controllers/orgAdmin.controller.js +491 -0
  42. package/src/controllers/stripeAdmin.controller.js +410 -0
  43. package/src/controllers/user.controller.js +361 -0
  44. package/src/controllers/userAdmin.controller.js +277 -0
  45. package/src/controllers/waitingList.controller.js +167 -0
  46. package/src/controllers/webhook.controller.js +200 -0
  47. package/src/middleware/auth.js +66 -0
  48. package/src/middleware/errorCapture.js +170 -0
  49. package/src/middleware/headlessApiTokenAuth.js +57 -0
  50. package/src/middleware/org.js +108 -0
  51. package/src/middleware.js +901 -0
  52. package/src/models/ActionEvent.js +31 -0
  53. package/src/models/ActivityLog.js +41 -0
  54. package/src/models/Asset.js +84 -0
  55. package/src/models/AuditEvent.js +93 -0
  56. package/src/models/EmailLog.js +28 -0
  57. package/src/models/ErrorAggregate.js +72 -0
  58. package/src/models/FormSubmission.js +41 -0
  59. package/src/models/GlobalSetting.js +38 -0
  60. package/src/models/HeadlessApiToken.js +24 -0
  61. package/src/models/HeadlessModelDefinition.js +41 -0
  62. package/src/models/I18nEntry.js +77 -0
  63. package/src/models/I18nLocale.js +33 -0
  64. package/src/models/Invite.js +70 -0
  65. package/src/models/JsonConfig.js +46 -0
  66. package/src/models/Notification.js +60 -0
  67. package/src/models/Organization.js +57 -0
  68. package/src/models/OrganizationMember.js +43 -0
  69. package/src/models/StripeCatalogItem.js +77 -0
  70. package/src/models/StripeWebhookEvent.js +57 -0
  71. package/src/models/User.js +89 -0
  72. package/src/models/VirtualEjsFile.js +60 -0
  73. package/src/models/VirtualEjsFileVersion.js +43 -0
  74. package/src/models/VirtualEjsGroupChange.js +32 -0
  75. package/src/models/WaitingList.js +41 -0
  76. package/src/models/Webhook.js +63 -0
  77. package/src/models/Workflow.js +29 -0
  78. package/src/models/WorkflowExecution.js +12 -0
  79. package/src/routes/admin.routes.js +26 -0
  80. package/src/routes/adminAssets.routes.js +28 -0
  81. package/src/routes/adminAssetsStorage.routes.js +13 -0
  82. package/src/routes/adminAudit.routes.js +196 -0
  83. package/src/routes/adminEjsVirtual.routes.js +17 -0
  84. package/src/routes/adminErrors.routes.js +164 -0
  85. package/src/routes/adminFeatureFlags.routes.js +12 -0
  86. package/src/routes/adminHeadless.routes.js +38 -0
  87. package/src/routes/adminI18n.routes.js +22 -0
  88. package/src/routes/adminJsonConfigs.routes.js +15 -0
  89. package/src/routes/adminLlm.routes.js +12 -0
  90. package/src/routes/adminMigration.routes.js +81 -0
  91. package/src/routes/adminSeoConfig.routes.js +20 -0
  92. package/src/routes/adminUploadNamespaces.routes.js +13 -0
  93. package/src/routes/assets.routes.js +21 -0
  94. package/src/routes/auth.routes.js +12 -0
  95. package/src/routes/billing.routes.js +11 -0
  96. package/src/routes/errorTracking.routes.js +31 -0
  97. package/src/routes/featureFlags.routes.js +9 -0
  98. package/src/routes/forms.routes.js +9 -0
  99. package/src/routes/formsAdmin.routes.js +13 -0
  100. package/src/routes/globalSettings.routes.js +18 -0
  101. package/src/routes/headless.routes.js +15 -0
  102. package/src/routes/i18n.routes.js +8 -0
  103. package/src/routes/invite.routes.js +9 -0
  104. package/src/routes/jsonConfigs.routes.js +8 -0
  105. package/src/routes/log.routes.js +111 -0
  106. package/src/routes/metrics.routes.js +9 -0
  107. package/src/routes/notificationAdmin.routes.js +15 -0
  108. package/src/routes/notifications.routes.js +12 -0
  109. package/src/routes/org.routes.js +31 -0
  110. package/src/routes/orgAdmin.routes.js +20 -0
  111. package/src/routes/publicAssets.routes.js +7 -0
  112. package/src/routes/stripeAdmin.routes.js +20 -0
  113. package/src/routes/user.routes.js +22 -0
  114. package/src/routes/userAdmin.routes.js +15 -0
  115. package/src/routes/waitingList.routes.js +13 -0
  116. package/src/routes/waitingListAdmin.routes.js +9 -0
  117. package/src/routes/webhook.routes.js +32 -0
  118. package/src/routes/workflowWebhook.routes.js +54 -0
  119. package/src/routes/workflows.routes.js +110 -0
  120. package/src/services/assets.service.js +110 -0
  121. package/src/services/audit.service.js +62 -0
  122. package/src/services/auditLogger.js +165 -0
  123. package/src/services/ejsVirtual.service.js +614 -0
  124. package/src/services/email.service.js +351 -0
  125. package/src/services/errorLogger.js +221 -0
  126. package/src/services/featureFlags.service.js +202 -0
  127. package/src/services/forms.service.js +214 -0
  128. package/src/services/globalSettings.service.js +49 -0
  129. package/src/services/headlessApiTokens.service.js +158 -0
  130. package/src/services/headlessCrypto.service.js +31 -0
  131. package/src/services/headlessModels.service.js +356 -0
  132. package/src/services/i18n.service.js +314 -0
  133. package/src/services/i18nInferredKeys.service.js +337 -0
  134. package/src/services/jsonConfigs.service.js +392 -0
  135. package/src/services/llm.service.js +749 -0
  136. package/src/services/migration.service.js +581 -0
  137. package/src/services/migrationAssets/fsLocal.js +58 -0
  138. package/src/services/migrationAssets/index.js +134 -0
  139. package/src/services/migrationAssets/s3.js +75 -0
  140. package/src/services/migrationAssets/sftp.js +92 -0
  141. package/src/services/notification.service.js +212 -0
  142. package/src/services/objectStorage.service.js +514 -0
  143. package/src/services/seoConfig.service.js +402 -0
  144. package/src/services/storage.js +150 -0
  145. package/src/services/stripe.service.js +185 -0
  146. package/src/services/stripeHelper.service.js +264 -0
  147. package/src/services/uploadNamespaces.service.js +326 -0
  148. package/src/services/webhook.service.js +157 -0
  149. package/src/services/workflow.service.js +271 -0
  150. package/src/utils/asyncHandler.js +5 -0
  151. package/src/utils/encryption.js +80 -0
  152. package/src/utils/jwt.js +40 -0
  153. package/src/utils/orgRoles.js +156 -0
  154. package/src/utils/validation.js +26 -0
  155. package/src/utils/webhookRetry.js +93 -0
  156. package/views/admin-assets.ejs +444 -0
  157. package/views/admin-audit.ejs +283 -0
  158. package/views/admin-coolify-deploy.ejs +207 -0
  159. package/views/admin-dashboard-home.ejs +291 -0
  160. package/views/admin-dashboard.ejs +397 -0
  161. package/views/admin-ejs-virtual.ejs +280 -0
  162. package/views/admin-errors.ejs +368 -0
  163. package/views/admin-feature-flags.ejs +390 -0
  164. package/views/admin-forms.ejs +526 -0
  165. package/views/admin-global-settings.ejs +436 -0
  166. package/views/admin-headless.ejs +2020 -0
  167. package/views/admin-i18n-locales.ejs +221 -0
  168. package/views/admin-i18n.ejs +728 -0
  169. package/views/admin-json-configs.ejs +410 -0
  170. package/views/admin-llm.ejs +884 -0
  171. package/views/admin-metrics.ejs +274 -0
  172. package/views/admin-migration.ejs +814 -0
  173. package/views/admin-notifications.ejs +430 -0
  174. package/views/admin-organizations.ejs +984 -0
  175. package/views/admin-seo-config.ejs +673 -0
  176. package/views/admin-stripe-pricing.ejs +558 -0
  177. package/views/admin-test.ejs +342 -0
  178. package/views/admin-users.ejs +452 -0
  179. package/views/admin-waiting-list.ejs +547 -0
  180. package/views/admin-webhooks.ejs +329 -0
  181. package/views/admin-workflows.ejs +310 -0
  182. package/views/partials/admin-assets-script.ejs +2022 -0
  183. package/views/partials/admin-test-sidebar.ejs +14 -0
  184. package/views/partials/dashboard/nav-items.ejs +66 -0
  185. package/views/partials/dashboard/palette.ejs +63 -0
  186. package/views/partials/dashboard/sidebar.ejs +21 -0
  187. package/views/partials/dashboard/tab-bar.ejs +26 -0
  188. package/views/partials/footer.ejs +3 -0
@@ -0,0 +1,526 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Forms & Leads Admin</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
10
+ <style>
11
+ .toast {
12
+ animation: slideIn 0.3s ease-out;
13
+ }
14
+ @keyframes slideIn {
15
+ from { transform: translateX(100%); opacity: 0; }
16
+ to { transform: translateX(0); opacity: 1; }
17
+ }
18
+ .fade-out {
19
+ animation: fadeOut 0.3s ease-out forwards;
20
+ }
21
+ @keyframes fadeOut {
22
+ from { opacity: 1; }
23
+ to { opacity: 0; }
24
+ }
25
+ </style>
26
+ </head>
27
+ <body class="bg-gray-100">
28
+ <div id="app" class="min-h-screen">
29
+ <!-- Header -->
30
+ <div class="bg-white shadow">
31
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
32
+ <div class="flex justify-between items-center">
33
+ <div>
34
+ <h1 class="text-2xl font-bold text-gray-900">Forms & Leads</h1>
35
+ <p class="text-sm text-gray-600 mt-1">Manage form definitions and browse submissions</p>
36
+ </div>
37
+ <div class="flex border-b border-gray-200">
38
+ <button
39
+ @click="activeTab = 'forms'"
40
+ :class="['px-4 py-2 font-medium text-sm', activeTab === 'forms' ? 'border-b-2 border-indigo-500 text-indigo-600' : 'text-gray-500 hover:text-gray-700']"
41
+ >Definitions</button>
42
+ <button
43
+ @click="activeTab = 'submissions'; loadSubmissions()"
44
+ :class="['px-4 py-2 font-medium text-sm', activeTab === 'submissions' ? 'border-b-2 border-indigo-500 text-indigo-600' : 'text-gray-500 hover:text-gray-700']"
45
+ >Submissions</button>
46
+ <button
47
+ @click="activeTab = 'setup'"
48
+ :class="['px-4 py-2 font-medium text-sm', activeTab === 'setup' ? 'border-b-2 border-indigo-500 text-indigo-600' : 'text-gray-500 hover:text-gray-700']"
49
+ >Quick Setup</button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Main Content -->
56
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
57
+
58
+ <!-- Definitions Tab -->
59
+ <div v-if="activeTab === 'forms'">
60
+ <div class="flex justify-between mb-4">
61
+ <h2 class="text-lg font-semibold text-gray-900">Form Definitions</h2>
62
+ <button @click="editForm({})" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
63
+ <i class="ti ti-plus mr-2"></i>New Form
64
+ </button>
65
+ </div>
66
+
67
+ <div class="bg-white shadow rounded-lg overflow-hidden border border-gray-200">
68
+ <table class="min-w-full divide-y divide-gray-200">
69
+ <thead class="bg-gray-50">
70
+ <tr>
71
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
72
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID / Key</th>
73
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody class="bg-white divide-y divide-gray-200">
77
+ <tr v-for="form in forms" :key="form.id">
78
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">{{ form.name }}</td>
79
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><code>{{ form.id }}</code></td>
80
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
81
+ <button @click="editForm(form)" class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</button>
82
+ <button @click="deleteForm(form.id)" class="text-red-600 hover:text-red-900">Delete</button>
83
+ </td>
84
+ </tr>
85
+ <tr v-if="forms.length === 0">
86
+ <td colspan="3" class="px-6 py-8 text-center text-sm text-gray-500 italic">No forms defined yet. Create your first form to start capturing leads.</td>
87
+ </tr>
88
+ </tbody>
89
+ </table>
90
+ </div>
91
+
92
+ <!-- Inline Editor -->
93
+ <div v-if="editingForm" class="mt-8 bg-white shadow rounded-lg p-6 border border-indigo-200">
94
+ <h3 class="text-lg font-medium text-gray-900 mb-4">{{ editingForm.id ? 'Edit' : 'Create' }} Form</h3>
95
+ <div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
96
+ <div class="sm:col-span-3">
97
+ <label class="block text-sm font-medium text-gray-700">Form Name</label>
98
+ <input v-model="editingForm.name" type="text" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
99
+ </div>
100
+ <div class="sm:col-span-3">
101
+ <label class="block text-sm font-medium text-gray-700">Custom ID (Optional)</label>
102
+ <input v-model="editingForm.id" type="text" :disabled="!!editingForm._id" placeholder="e.g. contact-form" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
103
+ </div>
104
+ <div class="sm:col-span-6">
105
+ <label class="block text-sm font-medium text-gray-700">Organization ID (for Webhooks)</label>
106
+ <input v-model="editingForm.organizationId" type="text" placeholder="org_..." class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
107
+ </div>
108
+ <div class="sm:col-span-6">
109
+ <label class="block text-sm font-medium text-gray-700">Success Redirect URL</label>
110
+ <input v-model="editingForm.successUrl" type="text" placeholder="https://example.com/thank-you" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
111
+ </div>
112
+ <div class="sm:col-span-6">
113
+ <label class="block text-sm font-medium text-gray-700">Webhook URL (Optional)</label>
114
+ <input v-model="editingForm.webhookUrl" type="text" placeholder="https://hooks.zapier.com/..." class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
115
+ </div>
116
+ <div class="sm:col-span-6">
117
+ <label class="block text-sm font-medium text-gray-700">Notification Email (Optional)</label>
118
+ <input v-model="editingForm.notifyEmail" type="email" placeholder="leads@example.com" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
119
+ </div>
120
+ </div>
121
+ <div class="mt-6 flex justify-end space-x-3">
122
+ <button @click="editingForm = null" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</button>
123
+ <button @click="saveForm" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700">Save Form</button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Submissions Tab -->
129
+ <div v-if="activeTab === 'submissions'">
130
+ <div class="bg-white rounded-lg shadow p-6 mb-6">
131
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-end">
132
+ <div class="flex-1">
133
+ <label class="block text-sm font-medium text-gray-700 mb-1">Filter by Form</label>
134
+ <select v-model="filter.formId" class="w-full border rounded px-3 py-2">
135
+ <option :value="null">All Forms</option>
136
+ <option v-for="f in forms" :value="f.id">{{ f.name }}</option>
137
+ </select>
138
+ </div>
139
+ <div class="flex gap-2">
140
+ <button @click="loadSubmissions" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">Apply Filters</button>
141
+ <button @click="resetFilters" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Reset</button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <div class="bg-white rounded-lg shadow overflow-hidden">
147
+ <div class="px-6 py-4 border-b flex items-center justify-between">
148
+ <h2 class="text-lg font-semibold text-gray-900">Captured Leads</h2>
149
+ <div class="flex items-center gap-2">
150
+ <span class="text-sm text-gray-500">Total: {{ pagination.total }}</span>
151
+ <button @click="prevPage" :disabled="filter.offset <= 0" class="p-1 disabled:opacity-50"><i class="ti ti-chevron-left"></i></button>
152
+ <button @click="nextPage" :disabled="filter.offset + filter.limit >= pagination.total" class="p-1 disabled:opacity-50"><i class="ti ti-chevron-right"></i></button>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="overflow-x-auto">
157
+ <table class="min-w-full divide-y divide-gray-200">
158
+ <thead class="bg-gray-50">
159
+ <tr>
160
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
161
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Form</th>
162
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP / Origin</th>
163
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th>
164
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
165
+ </tr>
166
+ </thead>
167
+ <tbody class="bg-white divide-y divide-gray-200">
168
+ <template v-for="sub in submissions" :key="sub._id">
169
+ <tr>
170
+ <td class="px-6 py-4 whitespace-nowrap text-xs text-gray-500">{{ formatDate(sub.createdAt) }}</td>
171
+ <td class="px-6 py-4 whitespace-nowrap text-xs text-indigo-600 font-semibold">{{ getFormName(sub.formKey) }}</td>
172
+ <td class="px-6 py-4 whitespace-nowrap text-xs text-gray-500">{{ sub.meta?.ip }}</td>
173
+ <td class="px-6 py-4 text-xs text-gray-900 max-w-xs truncate">{{ formatFields(sub.fields) }}</td>
174
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
175
+ <button @click="toggleDetails(sub._id)" class="text-indigo-600 hover:text-indigo-900 mr-3">Details</button>
176
+ <button @click="deleteSubmission(sub._id)" class="text-red-600 hover:text-red-900"><i class="ti ti-trash"></i></button>
177
+ </td>
178
+ </tr>
179
+ <tr v-if="expandedId === sub._id" class="bg-gray-50">
180
+ <td colspan="5" class="px-6 py-4">
181
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
182
+ <div>
183
+ <h4 class="text-xs font-bold text-gray-700 uppercase mb-2">Form Fields</h4>
184
+ <pre class="bg-white border rounded p-3 text-[10px] overflow-auto max-h-40">{{ sub.fields }}</pre>
185
+ </div>
186
+ <div>
187
+ <h4 class="text-xs font-bold text-gray-700 uppercase mb-2">Meta / Context</h4>
188
+ <pre class="bg-white border rounded p-3 text-[10px] overflow-auto max-h-40">{{ sub.meta }}</pre>
189
+ </div>
190
+ </div>
191
+ </td>
192
+ </tr>
193
+ </template>
194
+ <tr v-if="submissions.length === 0">
195
+ <td colspan="5" class="px-6 py-10 text-center text-sm text-gray-500">No submissions found matching your filters.</td>
196
+ </tr>
197
+ </tbody>
198
+ </table>
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- Setup Tab -->
204
+ <div v-if="activeTab === 'setup'" class="space-y-8">
205
+ <div class="bg-blue-50 border-l-4 border-blue-400 p-4">
206
+ <p class="text-sm text-blue-700">Select a form definition below to get the integration code for your external website.</p>
207
+ </div>
208
+
209
+ <section>
210
+ <label class="block text-sm font-medium text-gray-700 mb-2">1. Choose a Form</label>
211
+ <select v-model="setupFormId" class="block w-full border border-gray-300 rounded-md p-2 focus:ring-indigo-500 focus:border-indigo-500">
212
+ <option :value="null">Select a form...</option>
213
+ <option v-for="f in forms" :value="f.id">{{ f.name }}</option>
214
+ </select>
215
+ </section>
216
+
217
+ <div v-if="setupFormId" class="space-y-8">
218
+ <section>
219
+ <h4 class="text-md font-bold text-gray-900 mb-2">2. Styled HTML Form (Tailwind CSS)</h4>
220
+ <p class="text-sm text-gray-600 mb-2">A modern, production-ready form using Tailwind CSS classes.</p>
221
+ <div class="relative">
222
+ <pre class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs overflow-x-auto"><code>&lt;form action="{{ origin }}<%= baseUrl %>/api/forms/submit/{{ setupFormId }}" method="POST" class="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg space-y-4 border border-gray-100"&gt;
223
+ &lt;div&gt;
224
+ &lt;label class="block text-sm font-semibold text-gray-700 mb-1"&gt;Email Address&lt;/label&gt;
225
+ &lt;input type="email" name="email" placeholder="alex@example.com" required
226
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"&gt;
227
+ &lt;/div&gt;
228
+ &lt;div&gt;
229
+ &lt;label class="block text-sm font-semibold text-gray-700 mb-1"&gt;Message&lt;/label&gt;
230
+ &lt;textarea name="message" rows="4" placeholder="How can we help?"
231
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"&gt;&lt;/textarea&gt;
232
+ &lt;/div&gt;
233
+ &lt;button type="submit"
234
+ class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition-all active:scale-95"&gt;
235
+ Send Message
236
+ &lt;/button&gt;
237
+ &lt;/form&gt;</code></pre>
238
+ <button @click="copyCode('html')" class="absolute top-2 right-2 p-1 bg-gray-700 hover:bg-gray-600 rounded text-white transition">
239
+ <i class="ti ti-copy"></i>
240
+ </button>
241
+ </div>
242
+ </section>
243
+
244
+ <section>
245
+ <h4 class="text-md font-bold text-gray-900 mb-2">3. AJAX / Fetch Integration</h4>
246
+ <p class="text-sm text-gray-600 mb-2">Use this for a modern, refresh-less experience.</p>
247
+ <div class="relative">
248
+ <pre class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs overflow-x-auto"><code>const data = { email: 'user@example.com', message: 'Hello!' };
249
+
250
+ fetch('{{ origin }}<%= baseUrl %>/api/forms/submit/{{ setupFormId }}', {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify(data)
254
+ })
255
+ .then(res => res.json())
256
+ .then(data => console.log('Success!', data));</code></pre>
257
+ <button @click="copyCode('js')" class="absolute top-2 right-2 p-1 bg-gray-700 hover:bg-gray-600 rounded text-white transition">
258
+ <i class="ti ti-copy"></i>
259
+ </button>
260
+ </div>
261
+ </section>
262
+
263
+ <section>
264
+ <h4 class="text-md font-bold text-gray-900 mb-2">4. Zero-JS Refresh-less Integration (Hidden IFrame)</h4>
265
+ <p class="text-sm text-gray-600 mb-2">Use this if you want a submission without a page refresh, but don't want to write any JavaScript.</p>
266
+ <div class="relative">
267
+ <pre class="bg-gray-900 text-gray-100 p-4 rounded-lg text-xs overflow-x-auto"><code>&lt;!-- Hidden iframe to handle the response --&gt;
268
+ &lt;iframe name="hidden_iframe" id="hidden_iframe" style="display:none;" onload="if(this.contentWindow.name=='submitted'){alert('Form Submitted!');}"&gt;&lt;/iframe&gt;
269
+
270
+ &lt;!-- Form targeting the iframe --&gt;
271
+ &lt;form action="{{ origin }}<%= baseUrl %>/api/forms/submit/{{ setupFormId }}" method="POST" target="hidden_iframe" onsubmit="this.nextElementSibling.style.display='block'; this.style.display='none';" class="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg space-y-4 border border-gray-100"&gt;
272
+ &lt;div&gt;
273
+ &lt;label class="block text-sm font-semibold text-gray-700 mb-1"&gt;Email Address&lt;/label&gt;
274
+ &lt;input type="email" name="email" placeholder="alex@example.com" required
275
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"&gt;
276
+ &lt;/div&gt;
277
+ &lt;div&gt;
278
+ &lt;label class="block text-sm font-semibold text-gray-700 mb-1"&gt;Message&lt;/label&gt;
279
+ &lt;textarea name="message" rows="4" placeholder="How can we help?"
280
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"&gt;&lt;/textarea&gt;
281
+ &lt;/div&gt;
282
+ &lt;button type="submit"
283
+ class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition-all active:scale-95"&gt;
284
+ Send Message
285
+ &lt;/button&gt;
286
+ &lt;/form&gt;
287
+ &lt;div style="display:none;" class="text-center p-10 bg-white rounded-xl shadow-lg border border-gray-100"&gt;
288
+ &lt;h3 class="text-xl font-bold text-green-600"&gt;Thank you!&lt;/h3&gt;
289
+ &lt;p class="text-gray-500 mt-2"&gt;Your message has been received.&lt;/p&gt;
290
+ &lt;/div&gt;</code></pre>
291
+ <button @click="copyCode('iframe')" class="absolute top-2 right-2 p-1 bg-gray-700 hover:bg-gray-600 rounded text-white transition">
292
+ <i class="ti ti-copy"></i>
293
+ </button>
294
+ </div>
295
+ </section>
296
+ </div>
297
+
298
+ <div v-else class="text-center py-20 border-2 border-dashed border-gray-200 rounded-lg">
299
+ <i class="ti ti-hand-finger text-5xl text-gray-300 mb-4 block"></i>
300
+ <p class="text-gray-500">Pick a form above to see setup options</p>
301
+ </div>
302
+ </div>
303
+
304
+ </div>
305
+
306
+ <!-- Toast Container -->
307
+ <div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50">
308
+ <div v-for="t in toasts" :key="t.id" :class="['toast px-6 py-4 rounded-lg shadow-lg text-white transition-all duration-300', t.type === 'success' ? 'bg-green-500' : 'bg-red-500']">
309
+ {{ t.message }}
310
+ </div>
311
+ </div>
312
+ </div>
313
+
314
+ <script>
315
+ const { createApp, ref, onMounted, computed } = Vue;
316
+
317
+ createApp({
318
+ setup() {
319
+ const activeTab = ref('submissions');
320
+ const forms = ref([]);
321
+ const submissions = ref([]);
322
+ const editingForm = ref(null);
323
+ const expandedId = ref(null);
324
+ const setupFormId = ref(null);
325
+ const toasts = ref([]);
326
+ const filter = ref({
327
+ formId: null,
328
+ limit: 50,
329
+ offset: 0
330
+ });
331
+ const pagination = ref({
332
+ total: 0
333
+ });
334
+
335
+ const origin = window.location.origin;
336
+
337
+ const showToast = (message, type = 'success') => {
338
+ const id = Date.now();
339
+ toasts.value.push({ id, message, type });
340
+ setTimeout(() => {
341
+ toasts.value = toasts.value.filter(t => t.id !== id);
342
+ }, 3000);
343
+ };
344
+
345
+ const loadForms = async () => {
346
+ try {
347
+ const res = await fetch(`${window.location.origin}<%= baseUrl %>/api/admin/forms/definitions`);
348
+ forms.value = await res.json();
349
+ } catch (e) {
350
+ showToast('Failed to load forms', 'error');
351
+ }
352
+ };
353
+
354
+ const loadSubmissions = async () => {
355
+ try {
356
+ const params = new URLSearchParams();
357
+ if (filter.value.formId) params.append('formId', filter.value.formId);
358
+ params.append('limit', filter.value.limit);
359
+ params.append('offset', filter.value.offset);
360
+
361
+ const res = await fetch(`${window.location.origin}<%= baseUrl %>/api/admin/forms?${params.toString()}`);
362
+ const data = await res.json();
363
+ submissions.value = data.submissions || [];
364
+ pagination.value = data.pagination || { total: 0 };
365
+ } catch (e) {
366
+ showToast('Failed to load submissions', 'error');
367
+ }
368
+ };
369
+
370
+ const saveForm = async () => {
371
+ try {
372
+ const res = await fetch(`${window.location.origin}<%= baseUrl %>/api/admin/forms/definitions`, {
373
+ method: 'POST',
374
+ headers: { 'Content-Type': 'application/json' },
375
+ body: JSON.stringify(editingForm.value)
376
+ });
377
+ if (res.ok) {
378
+ showToast('Form saved successfully');
379
+ editingForm.value = null;
380
+ loadForms();
381
+ }
382
+ } catch (e) {
383
+ showToast('Failed to save form', 'error');
384
+ }
385
+ };
386
+
387
+ const deleteForm = async (id) => {
388
+ if (!confirm('Are you sure you want to delete this form definition? Submissions will remain but the integration might break.')) return;
389
+ try {
390
+ const res = await fetch(`${window.location.origin}<%= baseUrl %>/api/admin/forms/definitions/${id}`, { method: 'DELETE' });
391
+ if (res.ok) {
392
+ showToast('Form deleted');
393
+ loadForms();
394
+ }
395
+ } catch (e) {
396
+ showToast('Failed to delete form', 'error');
397
+ }
398
+ };
399
+
400
+ const deleteSubmission = async (id) => {
401
+ if (!confirm('Are you sure you want to delete this lead? This action cannot be undone.')) return;
402
+ try {
403
+ const res = await fetch(`${window.location.origin}<%= baseUrl %>/api/admin/forms/${id}`, { method: 'DELETE' });
404
+ if (res.ok) {
405
+ showToast('Submission deleted');
406
+ loadSubmissions();
407
+ }
408
+ } catch (e) {
409
+ showToast('Failed to delete submission', 'error');
410
+ }
411
+ };
412
+
413
+ const editForm = (form) => {
414
+ editingForm.value = { ...form };
415
+ };
416
+
417
+ const toggleDetails = (id) => {
418
+ expandedId.value = expandedId.value === id ? null : id;
419
+ };
420
+
421
+ const resetFilters = () => {
422
+ filter.value = { formId: null, limit: 50, offset: 0 };
423
+ loadSubmissions();
424
+ };
425
+
426
+ const formatDate = (iso) => new Date(iso).toLocaleString();
427
+
428
+ const getFormName = (id) => {
429
+ const f = forms.value.find(f => f.id === id);
430
+ return f ? f.name : id;
431
+ };
432
+
433
+ const formatFields = (fields) => {
434
+ if (!fields) return '';
435
+ return Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join(', ');
436
+ };
437
+
438
+ const prevPage = () => {
439
+ if (filter.value.offset > 0) {
440
+ filter.value.offset = Math.max(0, filter.value.offset - filter.value.limit);
441
+ loadSubmissions();
442
+ }
443
+ };
444
+
445
+ const nextPage = () => {
446
+ if (filter.value.offset + filter.value.limit < pagination.value.total) {
447
+ filter.value.offset += filter.value.limit;
448
+ loadSubmissions();
449
+ }
450
+ };
451
+
452
+ const copyCode = (type) => {
453
+ const origin = window.location.origin;
454
+ let code = '';
455
+ if (type === 'html') {
456
+ code = `<form action="${origin}<%= baseUrl %>/api/forms/submit/${setupFormId.value}" method="POST" class="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg space-y-4 border border-gray-100">
457
+ <div>
458
+ <label class="block text-sm font-semibold text-gray-700 mb-1">Email Address</label>
459
+ <input type="email" name="email" placeholder="alex@example.com" required
460
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all">
461
+ </div>
462
+ <div>
463
+ <label class="block text-sm font-semibold text-gray-700 mb-1">Message</label>
464
+ <textarea name="message" rows="4" placeholder="How can we help?"
465
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"></textarea>
466
+ </div>
467
+ <button type="submit"
468
+ class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition-all active:scale-95">
469
+ Send Message
470
+ </button>
471
+ </form>`;
472
+ } else if (type === 'iframe') {
473
+ code = `<!-- Hidden iframe to handle the response -->
474
+ <iframe name="hidden_iframe" id="hidden_iframe" style="display:none;" onload="if(this.contentWindow.name=='submitted'){alert('Form Submitted!');}"></iframe>
475
+
476
+ <!-- Form targeting the iframe -->
477
+ <form action="${origin}<%= baseUrl %>/api/forms/submit/${setupFormId.value}" method="POST" target="hidden_iframe" onsubmit="this.nextElementSibling.style.display='block'; this.style.display='none';" class="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg space-y-4 border border-gray-100">
478
+ <div>
479
+ <label class="block text-sm font-semibold text-gray-700 mb-1">Email Address</label>
480
+ <input type="email" name="email" placeholder="alex@example.com" required
481
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all">
482
+ </div>
483
+ <div>
484
+ <label class="block text-sm font-semibold text-gray-700 mb-1">Message</label>
485
+ <textarea name="message" rows="4" placeholder="How can we help?"
486
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"></textarea>
487
+ </div>
488
+ <button type="submit"
489
+ class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition-all active:scale-95">
490
+ Send Message
491
+ </button>
492
+ </form>
493
+ <div style="display:none;" class="text-center p-10 bg-white rounded-xl shadow-lg border border-gray-100">
494
+ <h3 class="text-xl font-bold text-green-600">Thank you!</h3>
495
+ <p class="text-gray-500 mt-2">Your message has been received.</p>
496
+ </div>`;
497
+ } else {
498
+ code = `const data = { email: 'user@example.com', message: 'Hello!' };
499
+
500
+ fetch('${origin}<%= baseUrl %>/api/forms/submit/${setupFormId.value}', {
501
+ method: 'POST',
502
+ headers: { 'Content-Type': 'application/json' },
503
+ body: JSON.stringify(data)
504
+ })
505
+ .then(res => res.json())
506
+ .then(data => console.log('Success!', data));`;
507
+ }
508
+ navigator.clipboard.writeText(code).then(() => showToast('Copied to clipboard'));
509
+ };
510
+
511
+ onMounted(() => {
512
+ loadForms();
513
+ loadSubmissions();
514
+ });
515
+
516
+ return {
517
+ activeTab, forms, submissions, editingForm, expandedId, setupFormId, filter, pagination, toasts,
518
+ loadSubmissions, saveForm, deleteForm, deleteSubmission, editForm, toggleDetails, resetFilters,
519
+ formatDate, getFormName, formatFields, prevPage, nextPage, copyCode,
520
+ origin
521
+ };
522
+ }
523
+ }).mount('#app');
524
+ </script>
525
+ </body>
526
+ </html>