@spaceinvoices/react-ui 0.1.1

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 (352) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +340 -0
  3. package/cli/dist/index.js +922 -0
  4. package/package.json +87 -0
  5. package/registry.json +600 -0
  6. package/spaceinvoices.schema.json +47 -0
  7. package/src/app.tsx +25 -0
  8. package/src/common/autocomplete.tsx +135 -0
  9. package/src/components/activities/activity-timeline.tsx +160 -0
  10. package/src/components/activities/index.ts +1 -0
  11. package/src/components/activities/locales/de.ts +30 -0
  12. package/src/components/activities/locales/sl.ts +30 -0
  13. package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
  14. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
  15. package/src/components/advance-invoices/create/locales/de.ts +29 -0
  16. package/src/components/advance-invoices/create/locales/sl.ts +25 -0
  17. package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
  18. package/src/components/advance-invoices/index.ts +5 -0
  19. package/src/components/advance-invoices/list/index.ts +3 -0
  20. package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
  21. package/src/components/advance-invoices/list/list-table.tsx +178 -0
  22. package/src/components/advance-invoices/list/locales/de.ts +32 -0
  23. package/src/components/advance-invoices/list/locales/sl.ts +32 -0
  24. package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
  25. package/src/components/button-loader.tsx +11 -0
  26. package/src/components/combobox.tsx +96 -0
  27. package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
  28. package/src/components/company-registry/company-registry.hooks.ts +67 -0
  29. package/src/components/company-registry/index.ts +7 -0
  30. package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
  31. package/src/components/credit-notes/create/index.ts +1 -0
  32. package/src/components/credit-notes/create/locales/de.ts +69 -0
  33. package/src/components/credit-notes/create/locales/sl.ts +67 -0
  34. package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
  35. package/src/components/credit-notes/index.ts +10 -0
  36. package/src/components/credit-notes/list/index.ts +3 -0
  37. package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
  38. package/src/components/credit-notes/list/list-table.tsx +183 -0
  39. package/src/components/credit-notes/list/locales/de.ts +33 -0
  40. package/src/components/credit-notes/list/locales/sl.ts +33 -0
  41. package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
  42. package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
  43. package/src/components/customers/create-customer-form/locales/de.ts +20 -0
  44. package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
  45. package/src/components/customers/customer-autocomplete.tsx +173 -0
  46. package/src/components/customers/customer-combobox.tsx +130 -0
  47. package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
  48. package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
  49. package/src/components/customers/customer-list-table/index.ts +2 -0
  50. package/src/components/customers/customer-list-table/locales/de.ts +16 -0
  51. package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
  52. package/src/components/customers/customers.hooks.test.ts +348 -0
  53. package/src/components/customers/customers.hooks.ts +57 -0
  54. package/src/components/customers/index.ts +5 -0
  55. package/src/components/dashboard/chart-empty-state.tsx +29 -0
  56. package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
  57. package/src/components/dashboard/collection-rate-card/index.ts +4 -0
  58. package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
  59. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
  60. package/src/components/dashboard/index.ts +54 -0
  61. package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
  62. package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
  63. package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
  64. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
  65. package/src/components/dashboard/loading-card.tsx +19 -0
  66. package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
  67. package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
  68. package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
  69. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
  70. package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
  71. package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
  72. package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
  73. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
  74. package/src/components/dashboard/revenue-card.tsx +49 -0
  75. package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
  76. package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
  77. package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
  78. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
  79. package/src/components/dashboard/shared/index.ts +5 -0
  80. package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
  81. package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
  82. package/src/components/dashboard/shared/use-stats-query.ts +38 -0
  83. package/src/components/dashboard/stat-card.tsx +41 -0
  84. package/src/components/dashboard/tax-collected-card/index.ts +2 -0
  85. package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
  86. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
  87. package/src/components/dashboard/top-customers-chart/index.ts +4 -0
  88. package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
  89. package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
  90. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
  91. package/src/components/documents/create/document-add-item-form.tsx +379 -0
  92. package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
  93. package/src/components/documents/create/document-details-section.tsx +597 -0
  94. package/src/components/documents/create/document-items-section.tsx +133 -0
  95. package/src/components/documents/create/document-recipient-section.tsx +101 -0
  96. package/src/components/documents/create/form-types.ts +36 -0
  97. package/src/components/documents/create/index.ts +9 -0
  98. package/src/components/documents/create/live-preview.tsx +235 -0
  99. package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
  100. package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
  101. package/src/components/documents/create/prepare-document-submission.ts +187 -0
  102. package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
  103. package/src/components/documents/create/prepare-preview-data.ts +16 -0
  104. package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
  105. package/src/components/documents/create/use-document-customer-form.ts +161 -0
  106. package/src/components/documents/document-preview.tsx +13 -0
  107. package/src/components/documents/documents.hooks.ts +146 -0
  108. package/src/components/documents/index.ts +23 -0
  109. package/src/components/documents/shared/document-preview-display.tsx +172 -0
  110. package/src/components/documents/shared/index.ts +3 -0
  111. package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
  112. package/src/components/documents/shared/use-a4-scaling.ts +62 -0
  113. package/src/components/documents/types.ts +61 -0
  114. package/src/components/documents/view/document-actions-bar.tsx +328 -0
  115. package/src/components/documents/view/document-details-card.tsx +179 -0
  116. package/src/components/documents/view/document-payments-list.tsx +256 -0
  117. package/src/components/documents/view/index.ts +4 -0
  118. package/src/components/documents/view/locales/de.ts +85 -0
  119. package/src/components/documents/view/locales/sl.ts +84 -0
  120. package/src/components/documents/view/use-document-download.ts +125 -0
  121. package/src/components/entities/create-entity-form.tsx +105 -0
  122. package/src/components/entities/entities.hooks.ts +50 -0
  123. package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
  124. package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
  125. package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
  126. package/src/components/entities/entity-settings-form/index.ts +2 -0
  127. package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
  128. package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
  129. package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
  130. package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
  131. package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
  132. package/src/components/entities/furs-settings-form/index.ts +3 -0
  133. package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
  134. package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
  135. package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
  136. package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
  137. package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
  138. package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
  139. package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
  140. package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
  141. package/src/components/entities/keys.ts +2 -0
  142. package/src/components/entities/settings/branding-settings-form.tsx +274 -0
  143. package/src/components/entities/settings/company-settings-form.tsx +256 -0
  144. package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
  145. package/src/components/entities/settings/email-settings-form.tsx +288 -0
  146. package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
  147. package/src/components/entities/settings/index.ts +8 -0
  148. package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
  149. package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
  150. package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
  151. package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
  152. package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
  153. package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
  154. package/src/components/entities/settings/settings-footer.tsx +16 -0
  155. package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
  156. package/src/components/estimates/create/create-estimate-form.tsx +384 -0
  157. package/src/components/estimates/create/locales/de.ts +64 -0
  158. package/src/components/estimates/create/locales/sl.ts +63 -0
  159. package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
  160. package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
  161. package/src/components/estimates/estimates.hooks.ts +15 -0
  162. package/src/components/estimates/index.ts +6 -0
  163. package/src/components/estimates/list/index.ts +3 -0
  164. package/src/components/estimates/list/list-row-actions.tsx +103 -0
  165. package/src/components/estimates/list/list-table.tsx +171 -0
  166. package/src/components/estimates/list/locales/de.ts +26 -0
  167. package/src/components/estimates/list/locales/sl.ts +26 -0
  168. package/src/components/estimates/list/use-estimate-download.ts +63 -0
  169. package/src/components/export/document-export-form.tsx +288 -0
  170. package/src/components/export/index.ts +2 -0
  171. package/src/components/form/form-input.tsx +89 -0
  172. package/src/components/form/index.ts +1 -0
  173. package/src/components/invoices/create/create-invoice-form.tsx +852 -0
  174. package/src/components/invoices/create/eslog-validation.test.ts +242 -0
  175. package/src/components/invoices/create/eslog-validation.ts +208 -0
  176. package/src/components/invoices/create/locales/de.ts +118 -0
  177. package/src/components/invoices/create/locales/sl.ts +114 -0
  178. package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
  179. package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
  180. package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
  181. package/src/components/invoices/index.ts +9 -0
  182. package/src/components/invoices/invoices-furs.hooks.ts +28 -0
  183. package/src/components/invoices/invoices.hooks.ts +110 -0
  184. package/src/components/invoices/list/index.ts +3 -0
  185. package/src/components/invoices/list/list-row-actions.tsx +132 -0
  186. package/src/components/invoices/list/list-table.tsx +165 -0
  187. package/src/components/invoices/list/locales/de.ts +33 -0
  188. package/src/components/invoices/list/locales/sl.ts +33 -0
  189. package/src/components/invoices/list/use-invoice-download.ts +62 -0
  190. package/src/components/invoices/send-email-dialog/index.ts +1 -0
  191. package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
  192. package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
  193. package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
  194. package/src/components/invoices/send-email-dialog.tsx +2 -0
  195. package/src/components/invoices/shared/index.ts +2 -0
  196. package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
  197. package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
  198. package/src/components/invoices/view/eslog-info-display.tsx +160 -0
  199. package/src/components/invoices/view/furs-info-display.tsx +213 -0
  200. package/src/components/items/create-item-form/create-item-form.tsx +155 -0
  201. package/src/components/items/create-item-form/locales/de.ts +14 -0
  202. package/src/components/items/create-item-form/locales/en.ts +9 -0
  203. package/src/components/items/create-item-form/locales/sl.ts +14 -0
  204. package/src/components/items/item-combobox.tsx +147 -0
  205. package/src/components/items/item-list-table/item-list-header.tsx +33 -0
  206. package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
  207. package/src/components/items/item-list-table/item-list-row.tsx +32 -0
  208. package/src/components/items/item-list-table/item-list-table.tsx +76 -0
  209. package/src/components/items/item-list-table/locales/de.ts +10 -0
  210. package/src/components/items/item-list-table/locales/en.ts +10 -0
  211. package/src/components/items/item-list-table/locales/sl.ts +10 -0
  212. package/src/components/items/items.hooks.ts +63 -0
  213. package/src/components/loading-spinner.tsx +24 -0
  214. package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
  215. package/src/components/payments/create-payment-form/locales/de.ts +20 -0
  216. package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
  217. package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
  218. package/src/components/payments/edit-payment-form/index.ts +1 -0
  219. package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
  220. package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
  221. package/src/components/payments/index.ts +4 -0
  222. package/src/components/payments/list/index.ts +2 -0
  223. package/src/components/payments/list/list-row-actions.tsx +98 -0
  224. package/src/components/payments/list/list-table.tsx +186 -0
  225. package/src/components/payments/list/locales/de.ts +19 -0
  226. package/src/components/payments/list/locales/sl.ts +19 -0
  227. package/src/components/payments/payments.hooks.ts +15 -0
  228. package/src/components/request-logs/index.ts +3 -0
  229. package/src/components/request-logs/request-log-detail.tsx +242 -0
  230. package/src/components/request-logs/request-log-list-table.tsx +266 -0
  231. package/src/components/request-logs/request-logs-page.tsx +10 -0
  232. package/src/components/table/README.md +410 -0
  233. package/src/components/table/data-table.tsx +251 -0
  234. package/src/components/table/date-cell.tsx +35 -0
  235. package/src/components/table/filter-bar.tsx +114 -0
  236. package/src/components/table/filter-panel.tsx +407 -0
  237. package/src/components/table/hooks/use-table-fetch.ts +17 -0
  238. package/src/components/table/hooks/use-table-query.ts +36 -0
  239. package/src/components/table/hooks/use-table-state.ts +293 -0
  240. package/src/components/table/index.ts +35 -0
  241. package/src/components/table/search-input.tsx +85 -0
  242. package/src/components/table/sortable-header.tsx +56 -0
  243. package/src/components/table/table-empty-state.tsx +40 -0
  244. package/src/components/table/table-no-results.tsx +41 -0
  245. package/src/components/table/table-pagination.tsx +42 -0
  246. package/src/components/table/table-skeleton.tsx +54 -0
  247. package/src/components/table/types.ts +136 -0
  248. package/src/components/tax-reports/index.ts +1 -0
  249. package/src/components/tax-reports/kir-export-form.tsx +172 -0
  250. package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
  251. package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
  252. package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
  253. package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
  254. package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
  255. package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
  256. package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
  257. package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
  258. package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
  259. package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
  260. package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
  261. package/src/components/taxes/taxes.hooks.ts +35 -0
  262. package/src/components/ui/alert-dialog.tsx +61 -0
  263. package/src/components/ui/alert.tsx +72 -0
  264. package/src/components/ui/badge.tsx +48 -0
  265. package/src/components/ui/breadcrumb.tsx +132 -0
  266. package/src/components/ui/button.tsx +61 -0
  267. package/src/components/ui/calendar.tsx +213 -0
  268. package/src/components/ui/card.tsx +94 -0
  269. package/src/components/ui/chart.tsx +380 -0
  270. package/src/components/ui/checkbox.tsx +27 -0
  271. package/src/components/ui/collapsible.tsx +56 -0
  272. package/src/components/ui/command.tsx +187 -0
  273. package/src/components/ui/dialog.tsx +187 -0
  274. package/src/components/ui/drawer.tsx +123 -0
  275. package/src/components/ui/dropdown-menu.tsx +291 -0
  276. package/src/components/ui/form.tsx +166 -0
  277. package/src/components/ui/input-group.tsx +149 -0
  278. package/src/components/ui/input.tsx +20 -0
  279. package/src/components/ui/label.tsx +18 -0
  280. package/src/components/ui/loading-spinner.tsx +16 -0
  281. package/src/components/ui/popover.tsx +108 -0
  282. package/src/components/ui/radio-group.tsx +37 -0
  283. package/src/components/ui/select.tsx +200 -0
  284. package/src/components/ui/separator.tsx +23 -0
  285. package/src/components/ui/sheet.tsx +145 -0
  286. package/src/components/ui/sidebar.tsx +771 -0
  287. package/src/components/ui/skeleton.tsx +13 -0
  288. package/src/components/ui/sonner.tsx +60 -0
  289. package/src/components/ui/spinner.tsx +10 -0
  290. package/src/components/ui/sticky-form-footer.tsx +55 -0
  291. package/src/components/ui/switch.tsx +30 -0
  292. package/src/components/ui/table.tsx +101 -0
  293. package/src/components/ui/tabs.tsx +80 -0
  294. package/src/components/ui/textarea.tsx +18 -0
  295. package/src/components/ui/tooltip.tsx +89 -0
  296. package/src/components/wl-subscription/index.ts +2 -0
  297. package/src/components/wl-subscription/locked-feature.tsx +173 -0
  298. package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
  299. package/src/frontend.tsx +28 -0
  300. package/src/generate-schemas.ts +265 -0
  301. package/src/generated/schemas/advanceinvoice.ts +177 -0
  302. package/src/generated/schemas/creditnote.ts +187 -0
  303. package/src/generated/schemas/customer.ts +29 -0
  304. package/src/generated/schemas/entity.ts +252 -0
  305. package/src/generated/schemas/estimate.ts +159 -0
  306. package/src/generated/schemas/furssettings.ts +25 -0
  307. package/src/generated/schemas/index.ts +24 -0
  308. package/src/generated/schemas/invoice.ts +167 -0
  309. package/src/generated/schemas/item.ts +38 -0
  310. package/src/generated/schemas/payment.ts +44 -0
  311. package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
  312. package/src/generated/schemas/previewestimate_body.ts +309 -0
  313. package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
  314. package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
  315. package/src/generated/schemas/renderdocument_body.ts +594 -0
  316. package/src/generated/schemas/sendemail_body.ts +26 -0
  317. package/src/generated/schemas/startpdfexport_body.ts +20 -0
  318. package/src/generated/schemas/tax.ts +48 -0
  319. package/src/generated/schemas/uploadfile_body.ts +23 -0
  320. package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
  321. package/src/generated/schemas/userfurssettings.ts +19 -0
  322. package/src/hooks/create-resource-hooks.test.ts +483 -0
  323. package/src/hooks/create-resource-hooks.ts +300 -0
  324. package/src/hooks/use-debounce.ts +12 -0
  325. package/src/hooks/use-duplicate-document.ts +185 -0
  326. package/src/hooks/use-media-query.tsx +19 -0
  327. package/src/hooks/use-mobile.ts +39 -0
  328. package/src/hooks/use-next-document-number.ts +57 -0
  329. package/src/hooks/use-resource-mutation.ts +118 -0
  330. package/src/hooks/use-vies-check.ts +130 -0
  331. package/src/index.css +11 -0
  332. package/src/index.html +13 -0
  333. package/src/index.tsx +12 -0
  334. package/src/lib/auth.ts +4 -0
  335. package/src/lib/browser-cookies.ts +70 -0
  336. package/src/lib/constants.ts +287 -0
  337. package/src/lib/cookies.ts +36 -0
  338. package/src/lib/schemas/advance-invoice.ts +43 -0
  339. package/src/lib/schemas/credit-note.ts +32 -0
  340. package/src/lib/schemas/estimate.ts +31 -0
  341. package/src/lib/schemas/index.ts +18 -0
  342. package/src/lib/schemas/invoice.ts +43 -0
  343. package/src/lib/schemas/shared.ts +79 -0
  344. package/src/lib/translation.ts +38 -0
  345. package/src/lib/utils.ts +6 -0
  346. package/src/providers/entities-context.tsx +41 -0
  347. package/src/providers/entities-provider.tsx +201 -0
  348. package/src/providers/form-footer-context.tsx +72 -0
  349. package/src/providers/sdk-provider.tsx +164 -0
  350. package/src/providers/white-label-provider.tsx +91 -0
  351. package/src/providers/wl-subscription-provider.tsx +277 -0
  352. package/src/utils/string-helpers.ts +111 -0
@@ -0,0 +1,293 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import type { FilterState, HttpMethodFilter, HttpStatusCodeFilter, StatusFilter, TableQueryParams } from "../types";
3
+
4
+ type UseTableStateProps = {
5
+ initialParams?: TableQueryParams;
6
+ defaultOrderBy?: string;
7
+ onChangeParams?: (params: TableQueryParams) => void;
8
+ /** When true, disables URL sync entirely (for embedded tables like dashboard) */
9
+ disableUrlSync?: boolean;
10
+ };
11
+
12
+ /**
13
+ * Format date to YYYY-MM-DD for API (using local timezone)
14
+ */
15
+ function formatDateForAPI(date: Date): string {
16
+ const year = date.getFullYear();
17
+ const month = String(date.getMonth() + 1).padStart(2, "0");
18
+ const day = String(date.getDate()).padStart(2, "0");
19
+ return `${year}-${month}-${day}`;
20
+ }
21
+
22
+ /**
23
+ * Parse YYYY-MM-DD string to local Date (avoids UTC parsing issues)
24
+ */
25
+ function parseDateString(dateStr: string): Date {
26
+ const [year, month, day] = dateStr.split("-").map(Number);
27
+ return new Date(year, month - 1, day);
28
+ }
29
+
30
+ /**
31
+ * Parse filter state from URL-friendly params
32
+ */
33
+ function parseFilterStateFromParams(params: TableQueryParams): FilterState | null {
34
+ const state: FilterState = {};
35
+
36
+ // Parse date filter
37
+ if (params.filter_date_field && (params.filter_date_from || params.filter_date_to)) {
38
+ state.dateFilter = {
39
+ field: params.filter_date_field,
40
+ range: {
41
+ from: params.filter_date_from ? parseDateString(params.filter_date_from) : undefined,
42
+ to: params.filter_date_to ? parseDateString(params.filter_date_to) : undefined,
43
+ },
44
+ };
45
+ }
46
+
47
+ // Parse status filter
48
+ if (params.filter_status) {
49
+ state.statusFilters = params.filter_status.split(",") as StatusFilter[];
50
+ }
51
+
52
+ // Parse HTTP method filter
53
+ if (params.filter_method) {
54
+ state.httpMethod = params.filter_method as HttpMethodFilter;
55
+ }
56
+
57
+ // Parse HTTP status code filter
58
+ if (params.filter_http_status) {
59
+ state.httpStatusCode = params.filter_http_status as HttpStatusCodeFilter;
60
+ }
61
+
62
+ return Object.keys(state).length > 0 ? state : null;
63
+ }
64
+
65
+ /**
66
+ * Build API query JSON from filter state
67
+ * Note: API only supports flat field-level queries, not AND/OR operators
68
+ * For multiple statuses, only the first is used (API limitation)
69
+ */
70
+ export function buildQueryFromFilterState(state: FilterState | null): string | undefined {
71
+ if (!state) return undefined;
72
+
73
+ const query: Record<string, unknown> = {};
74
+
75
+ // Date filter
76
+ if (state.dateFilter?.range.from || state.dateFilter?.range.to) {
77
+ const field = state.dateFilter.field;
78
+ const range = state.dateFilter.range;
79
+
80
+ if (range.from && range.to) {
81
+ query[field] = { between: [formatDateForAPI(range.from), formatDateForAPI(range.to)] };
82
+ } else if (range.from) {
83
+ query[field] = { gte: formatDateForAPI(range.from) };
84
+ } else if (range.to) {
85
+ query[field] = { lte: formatDateForAPI(range.to) };
86
+ }
87
+ }
88
+
89
+ // Status filter - apply first selected status only (API doesn't support OR)
90
+ if (state.statusFilters?.length) {
91
+ const status = state.statusFilters[0]; // Use first status
92
+ const today = formatDateForAPI(new Date());
93
+
94
+ switch (status) {
95
+ case "paid":
96
+ query.paid_in_full = { equals: true };
97
+ break;
98
+ case "unpaid":
99
+ query.paid_in_full = { equals: false };
100
+ query.voided_at = { equals: null };
101
+ break;
102
+ case "overdue":
103
+ query.paid_in_full = { equals: false };
104
+ query.date_due = { lt: today };
105
+ query.voided_at = { equals: null };
106
+ break;
107
+ case "voided":
108
+ query.voided_at = { not: null };
109
+ break;
110
+ }
111
+ }
112
+
113
+ if (Object.keys(query).length === 0) return undefined;
114
+ return JSON.stringify(query);
115
+ }
116
+
117
+ /**
118
+ * Manages table state (sorting, search, pagination) with optional URL sync
119
+ */
120
+ export function useTableState({
121
+ initialParams = {},
122
+ defaultOrderBy = "-id",
123
+ onChangeParams,
124
+ disableUrlSync = false,
125
+ }: UseTableStateProps) {
126
+ const [params, setParams] = useState<TableQueryParams>({
127
+ ...initialParams,
128
+ order_by: initialParams.order_by ?? defaultOrderBy,
129
+ });
130
+
131
+ // Use ref for onChangeParams to keep it stable
132
+ const onChangeParamsRef = useRef(onChangeParams);
133
+ onChangeParamsRef.current = onChangeParams;
134
+
135
+ // Keep track of previous initialParams to detect changes
136
+ const prevInitialParamsRef = useRef<string>(JSON.stringify(initialParams));
137
+ // Flag to track if we're updating from initialParams (to avoid calling onChangeParams)
138
+ const isUpdatingFromInitialRef = useRef(false);
139
+
140
+ // Sync internal state when initialParams changes (e.g., when navigating to same page resets URL)
141
+ useEffect(() => {
142
+ const currentParamsStr = JSON.stringify(initialParams);
143
+
144
+ // Only update if initialParams actually changed
145
+ if (currentParamsStr !== prevInitialParamsRef.current) {
146
+ prevInitialParamsRef.current = currentParamsStr;
147
+ isUpdatingFromInitialRef.current = true;
148
+ setParams({
149
+ ...initialParams,
150
+ order_by: initialParams.order_by ?? defaultOrderBy,
151
+ });
152
+ }
153
+ }, [initialParams, defaultOrderBy]);
154
+
155
+ // Sync params to parent or URL when they change
156
+ useEffect(() => {
157
+ // Skip if we're updating from initialParams to avoid infinite loop
158
+ if (isUpdatingFromInitialRef.current) {
159
+ isUpdatingFromInitialRef.current = false;
160
+ return;
161
+ }
162
+
163
+ // Skip URL sync entirely when disabled (e.g., dashboard embedded tables)
164
+ if (disableUrlSync) {
165
+ return;
166
+ }
167
+
168
+ const changeHandler = onChangeParamsRef.current;
169
+
170
+ if (changeHandler) {
171
+ // Notify parent of param changes (e.g., for router navigation)
172
+ changeHandler(params);
173
+ } else {
174
+ // Update URL directly
175
+ const searchParams = new URLSearchParams();
176
+
177
+ Object.entries(params).forEach(([key, value]) => {
178
+ if (value !== undefined && value !== null) {
179
+ searchParams.set(key, String(value));
180
+ }
181
+ });
182
+
183
+ const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
184
+ window.history.pushState({}, "", newUrl);
185
+ }
186
+ }, [params, disableUrlSync]);
187
+
188
+ /**
189
+ * Handle sort change
190
+ */
191
+ const handleSort = useCallback(
192
+ (order: string | null) => {
193
+ setParams((prevParams) => ({
194
+ ...prevParams,
195
+ order_by: order ?? defaultOrderBy,
196
+ prev_cursor: undefined,
197
+ next_cursor: undefined,
198
+ }));
199
+ },
200
+ [defaultOrderBy],
201
+ );
202
+
203
+ /**
204
+ * Handle search change
205
+ */
206
+ const handleSearch = useCallback((value: string | null) => {
207
+ setParams((prevParams) => ({
208
+ ...prevParams,
209
+ search: value?.trim() ?? undefined,
210
+ prev_cursor: undefined,
211
+ next_cursor: undefined,
212
+ }));
213
+ }, []);
214
+
215
+ /**
216
+ * Handle pagination change
217
+ */
218
+ const handlePageChange = useCallback((cursor: { prev?: string; next?: string }) => {
219
+ setParams((prevParams) => ({
220
+ ...prevParams,
221
+ next_cursor: cursor.next,
222
+ prev_cursor: cursor.next ? undefined : cursor.prev,
223
+ }));
224
+ }, []);
225
+
226
+ /**
227
+ * Handle filter change - stores URL-friendly params
228
+ */
229
+ const handleFilterChange = useCallback((state: FilterState | null) => {
230
+ setParams((prevParams) => ({
231
+ ...prevParams,
232
+ // Clear old filter params
233
+ filter_date_field: undefined,
234
+ filter_date_from: undefined,
235
+ filter_date_to: undefined,
236
+ filter_status: undefined,
237
+ filter_method: undefined,
238
+ filter_http_status: undefined,
239
+ // Set new filter params
240
+ ...(state?.dateFilter && {
241
+ filter_date_field: state.dateFilter.field,
242
+ filter_date_from: state.dateFilter.range.from ? formatDateForAPI(state.dateFilter.range.from) : undefined,
243
+ filter_date_to: state.dateFilter.range.to ? formatDateForAPI(state.dateFilter.range.to) : undefined,
244
+ }),
245
+ ...(state?.statusFilters?.length && {
246
+ filter_status: state.statusFilters.join(","),
247
+ }),
248
+ ...(state?.httpMethod && {
249
+ filter_method: state.httpMethod,
250
+ }),
251
+ ...(state?.httpStatusCode && {
252
+ filter_http_status: state.httpStatusCode,
253
+ }),
254
+ prev_cursor: undefined,
255
+ next_cursor: undefined,
256
+ }));
257
+ }, []);
258
+
259
+ /**
260
+ * Parse current filter state from URL params
261
+ */
262
+ const filterState = useMemo(() => {
263
+ return parseFilterStateFromParams(params);
264
+ }, [
265
+ params.filter_date_field,
266
+ params.filter_date_from,
267
+ params.filter_date_to,
268
+ params.filter_status,
269
+ params.filter_method,
270
+ params.filter_http_status,
271
+ params,
272
+ ]);
273
+
274
+ /**
275
+ * Build params for API call (includes query JSON built from filter state)
276
+ * Note: filter_* params are kept for tables that need them (like request logs)
277
+ * while query JSON is added for tables that use it (like invoices)
278
+ */
279
+ const apiParams = useMemo(() => {
280
+ const query = buildQueryFromFilterState(filterState);
281
+ return { ...params, query };
282
+ }, [params, filterState]);
283
+
284
+ return {
285
+ params,
286
+ apiParams,
287
+ filterState,
288
+ handleSort,
289
+ handleSearch,
290
+ handlePageChange,
291
+ handleFilterChange,
292
+ };
293
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Table Component Library
3
+ *
4
+ * A comprehensive, type-safe table system with built-in:
5
+ * - Sorting (client and server-side)
6
+ * - Search/filtering
7
+ * - Cursor-based pagination
8
+ * - Loading states
9
+ * - Empty states
10
+ * - TanStack Query integration
11
+ */
12
+
13
+ export type { DataTableProps } from "./data-table";
14
+ // Main component
15
+ export { DataTable } from "./data-table";
16
+ export { FormattedDate } from "./date-cell";
17
+ // Hooks
18
+ export { useTableFetch } from "./hooks/use-table-fetch";
19
+ export { useTableQuery } from "./hooks/use-table-query";
20
+ export { useTableState } from "./hooks/use-table-state";
21
+ // Supporting components
22
+ export { SearchInput } from "./search-input";
23
+ export { SortableHeader } from "./sortable-header";
24
+ export { TableEmptyState } from "./table-empty-state";
25
+ export { TableNoResults } from "./table-no-results";
26
+ export { Pagination } from "./table-pagination";
27
+ export { TableSkeleton } from "./table-skeleton";
28
+
29
+ // Types
30
+ export type {
31
+ Column,
32
+ ListTableProps,
33
+ TableQueryParams,
34
+ TableQueryResponse,
35
+ } from "./types";
@@ -0,0 +1,85 @@
1
+ import { Search, X } from "lucide-react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ import { Button } from "@/ui/components/ui/button";
5
+ import { Input } from "@/ui/components/ui/input";
6
+
7
+ type SearchInputProps = {
8
+ initialValue?: string;
9
+ onSearch: (value: string | null) => void;
10
+ placeholder?: string;
11
+ debounceMs?: number;
12
+ };
13
+
14
+ /**
15
+ * Search input with optional clear button and form submission
16
+ */
17
+ export function SearchInput({ initialValue = "", onSearch, placeholder = "Search...", debounceMs }: SearchInputProps) {
18
+ const [value, setValue] = useState(initialValue);
19
+
20
+ // Use ref to keep onSearch stable in useEffect
21
+ const onSearchRef = useRef(onSearch);
22
+ onSearchRef.current = onSearch;
23
+
24
+ // Sync with external value changes
25
+ useEffect(() => {
26
+ setValue(initialValue);
27
+ }, [initialValue]);
28
+
29
+ // Optional debounced search
30
+ useEffect(() => {
31
+ if (debounceMs === undefined) return;
32
+
33
+ const timer = setTimeout(() => {
34
+ onSearchRef.current(value || null);
35
+ }, debounceMs);
36
+
37
+ return () => clearTimeout(timer);
38
+ }, [value, debounceMs]);
39
+
40
+ const handleSubmit = useCallback(
41
+ (e: React.FormEvent) => {
42
+ e.preventDefault();
43
+ if (debounceMs === undefined) {
44
+ onSearchRef.current(value || null);
45
+ }
46
+ },
47
+ [debounceMs, value],
48
+ );
49
+
50
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
51
+ setValue(e.target.value);
52
+ }, []);
53
+
54
+ const handleClear = useCallback(() => {
55
+ setValue("");
56
+ onSearchRef.current(null);
57
+ }, []);
58
+
59
+ return (
60
+ <form onSubmit={handleSubmit} className="relative inline-block" data-testid="search-form">
61
+ <Search className="pointer-events-none absolute top-1/2 left-2.5 z-10 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
62
+ <Input
63
+ type="search"
64
+ role="searchbox"
65
+ aria-label="Search"
66
+ placeholder={placeholder}
67
+ value={value}
68
+ onChange={handleChange}
69
+ className="h-8 w-[150px] pr-8 pl-8 lg:w-[250px] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
70
+ />
71
+ {value && (
72
+ <Button
73
+ type="button"
74
+ variant="ghost"
75
+ size="sm"
76
+ onClick={handleClear}
77
+ className="absolute top-1/2 right-1 z-10 h-6 w-6 -translate-y-1/2 p-0 hover:bg-transparent"
78
+ aria-label="Clear search"
79
+ >
80
+ <X className="h-3 w-3 text-muted-foreground" />
81
+ </Button>
82
+ )}
83
+ </form>
84
+ );
85
+ }
@@ -0,0 +1,56 @@
1
+ import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
2
+
3
+ import { Button } from "@/ui/components/ui/button";
4
+ import { cn } from "@/ui/lib/utils";
5
+
6
+ type SortableHeaderProps = {
7
+ children: React.ReactNode;
8
+ field: string;
9
+ currentOrder?: string;
10
+ align?: "left" | "center" | "right";
11
+ onSort?: (order: string | null) => void;
12
+ };
13
+
14
+ /**
15
+ * Sortable column header with visual indicators
16
+ * Cycles through: none -> asc -> desc -> none
17
+ */
18
+ export function SortableHeader({ children, field, currentOrder, align = "left", onSort }: SortableHeaderProps) {
19
+ const isActive = currentOrder?.replace(/^-/, "") === field;
20
+ const isDesc = currentOrder?.startsWith("-");
21
+
22
+ // Determine next sort state
23
+ const getNextOrder = () => {
24
+ if (!isActive) return field; // Not active -> ascending
25
+ if (!isDesc) return `-${field}`; // Ascending -> descending
26
+ return null; // Descending -> none
27
+ };
28
+
29
+ const nextOrder = getNextOrder();
30
+
31
+ return (
32
+ <Button
33
+ variant="ghost"
34
+ size="sm"
35
+ onClick={() => onSort?.(nextOrder)}
36
+ className={cn(
37
+ "h-8 gap-1",
38
+ isActive && "font-semibold",
39
+ align === "left" && "-ml-3",
40
+ align === "right" && "-mr-3",
41
+ )}
42
+ aria-label={`Sort by ${field}${isActive ? (isDesc ? " descending" : " ascending") : ""}`}
43
+ >
44
+ {children}
45
+ {isActive ? (
46
+ isDesc ? (
47
+ <ArrowDown className="h-4 w-4" aria-hidden="true" />
48
+ ) : (
49
+ <ArrowUp className="h-4 w-4" aria-hidden="true" />
50
+ )
51
+ ) : (
52
+ <ArrowUpDown className="h-4 w-4 opacity-50" aria-hidden="true" />
53
+ )}
54
+ </Button>
55
+ );
56
+ }
@@ -0,0 +1,40 @@
1
+ import { Sprout } from "lucide-react";
2
+
3
+ import { Button } from "@/ui/components/ui/button";
4
+
5
+ type TableEmptyStateProps = {
6
+ resource: string;
7
+ createNewLink?: string;
8
+ createNewTrigger?: React.ReactNode;
9
+ /** Number of rows to calculate height (default: 10) */
10
+ rows?: number;
11
+ };
12
+
13
+ // Approximate row height in pixels (including padding/border)
14
+ const ROW_HEIGHT = 53;
15
+
16
+ /**
17
+ * Empty state shown when table has no data
18
+ */
19
+ export function TableEmptyState({ resource, createNewLink, createNewTrigger, rows = 10 }: TableEmptyStateProps) {
20
+ // Calculate height based on row count (min 200px)
21
+ const height = Math.max(rows * ROW_HEIGHT, 200);
22
+
23
+ return (
24
+ <div className="flex flex-col items-center justify-center gap-3" style={{ height }}>
25
+ <Sprout size={70} strokeWidth={0.35} className="text-muted-foreground" />
26
+ <div className="space-y-1 text-center">
27
+ <p className="font-light text-lg text-muted-foreground">Your {resource} list is empty</p>
28
+ {(createNewLink || createNewTrigger) && (
29
+ <p className="text-muted-foreground text-sm">Get started by creating your first {resource}</p>
30
+ )}
31
+ </div>
32
+ {createNewLink && (
33
+ <Button variant="default" size="sm" asChild>
34
+ <a href={createNewLink}>Create {resource}</a>
35
+ </Button>
36
+ )}
37
+ {createNewTrigger && !createNewLink && <div>{createNewTrigger}</div>}
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,41 @@
1
+ import { FileX } from "lucide-react";
2
+
3
+ import { Button } from "@/ui/components/ui/button";
4
+ import { TableCell, TableRow } from "@/ui/components/ui/table";
5
+
6
+ type TableNoResultsProps = {
7
+ resource: string;
8
+ search?: (value: null) => void;
9
+ /** Number of rows to calculate height (default: 10) */
10
+ rows?: number;
11
+ };
12
+
13
+ // Approximate row height in pixels (including padding/border)
14
+ const ROW_HEIGHT = 53;
15
+
16
+ /**
17
+ * No results message shown when search returns empty
18
+ */
19
+ export function TableNoResults({ resource, search, rows = 10 }: TableNoResultsProps) {
20
+ // Calculate height based on row count (min 150px)
21
+ const height = Math.max(rows * ROW_HEIGHT, 150);
22
+
23
+ return (
24
+ <TableRow className="hover:bg-transparent">
25
+ <TableCell colSpan={100} className="text-center align-middle" style={{ height }}>
26
+ <div className="flex flex-col items-center gap-3">
27
+ <FileX size={32} strokeWidth={1.5} className="text-muted-foreground" />
28
+ <div className="space-y-1">
29
+ <p className="font-medium text-muted-foreground">No {resource} found</p>
30
+ {search && <p className="text-muted-foreground text-sm">Try adjusting your search criteria</p>}
31
+ </div>
32
+ {search && (
33
+ <Button variant="link" size="sm" onClick={() => search(null)} className="underline">
34
+ Clear search
35
+ </Button>
36
+ )}
37
+ </div>
38
+ </TableCell>
39
+ </TableRow>
40
+ );
41
+ }
@@ -0,0 +1,42 @@
1
+ import { ChevronLeft, ChevronRight } from "lucide-react";
2
+
3
+ import { Button } from "@/ui/components/ui/button";
4
+
5
+ type PaginationProps = {
6
+ prevCursor?: string | null;
7
+ nextCursor?: string | null;
8
+ onPageChange: (cursor: { prev?: string; next?: string }) => void;
9
+ };
10
+
11
+ /**
12
+ * Cursor-based pagination controls
13
+ */
14
+ export function Pagination({ prevCursor, nextCursor, onPageChange }: PaginationProps) {
15
+ const hasPrevious = Boolean(prevCursor);
16
+ const hasNext = Boolean(nextCursor);
17
+
18
+ return (
19
+ <div className="flex items-center justify-end space-x-2">
20
+ <Button
21
+ variant="outline"
22
+ size="sm"
23
+ className="h-8 w-8 cursor-pointer p-0"
24
+ onClick={() => onPageChange({ prev: prevCursor ?? undefined })}
25
+ disabled={!hasPrevious}
26
+ aria-label="Previous page"
27
+ >
28
+ <ChevronLeft className="h-4 w-4" />
29
+ </Button>
30
+ <Button
31
+ variant="outline"
32
+ size="sm"
33
+ className="h-8 w-8 cursor-pointer p-0"
34
+ onClick={() => onPageChange({ next: nextCursor ?? undefined })}
35
+ disabled={!hasNext}
36
+ aria-label="Next page"
37
+ >
38
+ <ChevronRight className="h-4 w-4" />
39
+ </Button>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,54 @@
1
+ import { Skeleton } from "@/ui/components/ui/skeleton";
2
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/ui/components/ui/table";
3
+
4
+ type TableSkeletonProps = {
5
+ columns?: number;
6
+ rows?: number;
7
+ };
8
+
9
+ /**
10
+ * Loading skeleton for table component
11
+ */
12
+ export function TableSkeleton({ columns = 5, rows = 10 }: TableSkeletonProps) {
13
+ return (
14
+ <div className="space-y-4">
15
+ {/* Search input skeleton */}
16
+ <Skeleton className="h-8 w-[250px]" />
17
+
18
+ {/* Table skeleton */}
19
+ <div className="rounded-lg border">
20
+ <Table>
21
+ <TableHeader>
22
+ <TableRow>
23
+ {Array.from({ length: columns }, (_, i) => (
24
+ // biome-ignore lint/suspicious/noArrayIndexKey: skeleton
25
+ <TableHead key={`header-${i}`}>
26
+ <Skeleton data-testid="skeleton-cell" className="my-2 h-4 w-[100px]" />
27
+ </TableHead>
28
+ ))}
29
+ </TableRow>
30
+ </TableHeader>
31
+ <TableBody>
32
+ {Array.from({ length: rows }, (_, rowIndex) => (
33
+ // biome-ignore lint/suspicious/noArrayIndexKey: skeleton
34
+ <TableRow key={`row-${rowIndex}`}>
35
+ {Array.from({ length: columns }, (_, colIndex) => (
36
+ // biome-ignore lint/suspicious/noArrayIndexKey: skeleton
37
+ <TableCell key={`cell-${rowIndex}-${colIndex}`}>
38
+ <Skeleton data-testid="skeleton-cell" className="my-2 h-4 w-[100px]" />
39
+ </TableCell>
40
+ ))}
41
+ </TableRow>
42
+ ))}
43
+ </TableBody>
44
+ </Table>
45
+ </div>
46
+
47
+ {/* Pagination skeleton */}
48
+ <div className="flex justify-end gap-2">
49
+ <Skeleton className="h-8 w-8" />
50
+ <Skeleton className="h-8 w-8" />
51
+ </div>
52
+ </div>
53
+ );
54
+ }