@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.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/cli/dist/index.js +922 -0
- package/package.json +87 -0
- package/registry.json +600 -0
- package/spaceinvoices.schema.json +47 -0
- package/src/app.tsx +25 -0
- package/src/common/autocomplete.tsx +135 -0
- package/src/components/activities/activity-timeline.tsx +160 -0
- package/src/components/activities/index.ts +1 -0
- package/src/components/activities/locales/de.ts +30 -0
- package/src/components/activities/locales/sl.ts +30 -0
- package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
- package/src/components/advance-invoices/create/locales/de.ts +29 -0
- package/src/components/advance-invoices/create/locales/sl.ts +25 -0
- package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
- package/src/components/advance-invoices/index.ts +5 -0
- package/src/components/advance-invoices/list/index.ts +3 -0
- package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
- package/src/components/advance-invoices/list/list-table.tsx +178 -0
- package/src/components/advance-invoices/list/locales/de.ts +32 -0
- package/src/components/advance-invoices/list/locales/sl.ts +32 -0
- package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
- package/src/components/button-loader.tsx +11 -0
- package/src/components/combobox.tsx +96 -0
- package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
- package/src/components/company-registry/company-registry.hooks.ts +67 -0
- package/src/components/company-registry/index.ts +7 -0
- package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
- package/src/components/credit-notes/create/index.ts +1 -0
- package/src/components/credit-notes/create/locales/de.ts +69 -0
- package/src/components/credit-notes/create/locales/sl.ts +67 -0
- package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
- package/src/components/credit-notes/index.ts +10 -0
- package/src/components/credit-notes/list/index.ts +3 -0
- package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
- package/src/components/credit-notes/list/list-table.tsx +183 -0
- package/src/components/credit-notes/list/locales/de.ts +33 -0
- package/src/components/credit-notes/list/locales/sl.ts +33 -0
- package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
- package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
- package/src/components/customers/create-customer-form/locales/de.ts +20 -0
- package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
- package/src/components/customers/customer-autocomplete.tsx +173 -0
- package/src/components/customers/customer-combobox.tsx +130 -0
- package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
- package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
- package/src/components/customers/customer-list-table/index.ts +2 -0
- package/src/components/customers/customer-list-table/locales/de.ts +16 -0
- package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
- package/src/components/customers/customers.hooks.test.ts +348 -0
- package/src/components/customers/customers.hooks.ts +57 -0
- package/src/components/customers/index.ts +5 -0
- package/src/components/dashboard/chart-empty-state.tsx +29 -0
- package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
- package/src/components/dashboard/collection-rate-card/index.ts +4 -0
- package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
- package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
- package/src/components/dashboard/index.ts +54 -0
- package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
- package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
- package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
- package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
- package/src/components/dashboard/loading-card.tsx +19 -0
- package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
- package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
- package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
- package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
- package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
- package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
- package/src/components/dashboard/revenue-card.tsx +49 -0
- package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
- package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
- package/src/components/dashboard/shared/index.ts +5 -0
- package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
- package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
- package/src/components/dashboard/shared/use-stats-query.ts +38 -0
- package/src/components/dashboard/stat-card.tsx +41 -0
- package/src/components/dashboard/tax-collected-card/index.ts +2 -0
- package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
- package/src/components/dashboard/top-customers-chart/index.ts +4 -0
- package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
- package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
- package/src/components/documents/create/document-add-item-form.tsx +379 -0
- package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
- package/src/components/documents/create/document-details-section.tsx +597 -0
- package/src/components/documents/create/document-items-section.tsx +133 -0
- package/src/components/documents/create/document-recipient-section.tsx +101 -0
- package/src/components/documents/create/form-types.ts +36 -0
- package/src/components/documents/create/index.ts +9 -0
- package/src/components/documents/create/live-preview.tsx +235 -0
- package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
- package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
- package/src/components/documents/create/prepare-document-submission.ts +187 -0
- package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
- package/src/components/documents/create/prepare-preview-data.ts +16 -0
- package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
- package/src/components/documents/create/use-document-customer-form.ts +161 -0
- package/src/components/documents/document-preview.tsx +13 -0
- package/src/components/documents/documents.hooks.ts +146 -0
- package/src/components/documents/index.ts +23 -0
- package/src/components/documents/shared/document-preview-display.tsx +172 -0
- package/src/components/documents/shared/index.ts +3 -0
- package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
- package/src/components/documents/shared/use-a4-scaling.ts +62 -0
- package/src/components/documents/types.ts +61 -0
- package/src/components/documents/view/document-actions-bar.tsx +328 -0
- package/src/components/documents/view/document-details-card.tsx +179 -0
- package/src/components/documents/view/document-payments-list.tsx +256 -0
- package/src/components/documents/view/index.ts +4 -0
- package/src/components/documents/view/locales/de.ts +85 -0
- package/src/components/documents/view/locales/sl.ts +84 -0
- package/src/components/documents/view/use-document-download.ts +125 -0
- package/src/components/entities/create-entity-form.tsx +105 -0
- package/src/components/entities/entities.hooks.ts +50 -0
- package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
- package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
- package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
- package/src/components/entities/entity-settings-form/index.ts +2 -0
- package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
- package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
- package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
- package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
- package/src/components/entities/furs-settings-form/index.ts +3 -0
- package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
- package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
- package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
- package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
- package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
- package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
- package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
- package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
- package/src/components/entities/keys.ts +2 -0
- package/src/components/entities/settings/branding-settings-form.tsx +274 -0
- package/src/components/entities/settings/company-settings-form.tsx +256 -0
- package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
- package/src/components/entities/settings/email-settings-form.tsx +288 -0
- package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
- package/src/components/entities/settings/index.ts +8 -0
- package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
- package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
- package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
- package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
- package/src/components/entities/settings/settings-footer.tsx +16 -0
- package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
- package/src/components/estimates/create/create-estimate-form.tsx +384 -0
- package/src/components/estimates/create/locales/de.ts +64 -0
- package/src/components/estimates/create/locales/sl.ts +63 -0
- package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
- package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
- package/src/components/estimates/estimates.hooks.ts +15 -0
- package/src/components/estimates/index.ts +6 -0
- package/src/components/estimates/list/index.ts +3 -0
- package/src/components/estimates/list/list-row-actions.tsx +103 -0
- package/src/components/estimates/list/list-table.tsx +171 -0
- package/src/components/estimates/list/locales/de.ts +26 -0
- package/src/components/estimates/list/locales/sl.ts +26 -0
- package/src/components/estimates/list/use-estimate-download.ts +63 -0
- package/src/components/export/document-export-form.tsx +288 -0
- package/src/components/export/index.ts +2 -0
- package/src/components/form/form-input.tsx +89 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/invoices/create/create-invoice-form.tsx +852 -0
- package/src/components/invoices/create/eslog-validation.test.ts +242 -0
- package/src/components/invoices/create/eslog-validation.ts +208 -0
- package/src/components/invoices/create/locales/de.ts +118 -0
- package/src/components/invoices/create/locales/sl.ts +114 -0
- package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
- package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
- package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
- package/src/components/invoices/index.ts +9 -0
- package/src/components/invoices/invoices-furs.hooks.ts +28 -0
- package/src/components/invoices/invoices.hooks.ts +110 -0
- package/src/components/invoices/list/index.ts +3 -0
- package/src/components/invoices/list/list-row-actions.tsx +132 -0
- package/src/components/invoices/list/list-table.tsx +165 -0
- package/src/components/invoices/list/locales/de.ts +33 -0
- package/src/components/invoices/list/locales/sl.ts +33 -0
- package/src/components/invoices/list/use-invoice-download.ts +62 -0
- package/src/components/invoices/send-email-dialog/index.ts +1 -0
- package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
- package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
- package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
- package/src/components/invoices/send-email-dialog.tsx +2 -0
- package/src/components/invoices/shared/index.ts +2 -0
- package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
- package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
- package/src/components/invoices/view/eslog-info-display.tsx +160 -0
- package/src/components/invoices/view/furs-info-display.tsx +213 -0
- package/src/components/items/create-item-form/create-item-form.tsx +155 -0
- package/src/components/items/create-item-form/locales/de.ts +14 -0
- package/src/components/items/create-item-form/locales/en.ts +9 -0
- package/src/components/items/create-item-form/locales/sl.ts +14 -0
- package/src/components/items/item-combobox.tsx +147 -0
- package/src/components/items/item-list-table/item-list-header.tsx +33 -0
- package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
- package/src/components/items/item-list-table/item-list-row.tsx +32 -0
- package/src/components/items/item-list-table/item-list-table.tsx +76 -0
- package/src/components/items/item-list-table/locales/de.ts +10 -0
- package/src/components/items/item-list-table/locales/en.ts +10 -0
- package/src/components/items/item-list-table/locales/sl.ts +10 -0
- package/src/components/items/items.hooks.ts +63 -0
- package/src/components/loading-spinner.tsx +24 -0
- package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
- package/src/components/payments/create-payment-form/locales/de.ts +20 -0
- package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
- package/src/components/payments/edit-payment-form/index.ts +1 -0
- package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
- package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/index.ts +4 -0
- package/src/components/payments/list/index.ts +2 -0
- package/src/components/payments/list/list-row-actions.tsx +98 -0
- package/src/components/payments/list/list-table.tsx +186 -0
- package/src/components/payments/list/locales/de.ts +19 -0
- package/src/components/payments/list/locales/sl.ts +19 -0
- package/src/components/payments/payments.hooks.ts +15 -0
- package/src/components/request-logs/index.ts +3 -0
- package/src/components/request-logs/request-log-detail.tsx +242 -0
- package/src/components/request-logs/request-log-list-table.tsx +266 -0
- package/src/components/request-logs/request-logs-page.tsx +10 -0
- package/src/components/table/README.md +410 -0
- package/src/components/table/data-table.tsx +251 -0
- package/src/components/table/date-cell.tsx +35 -0
- package/src/components/table/filter-bar.tsx +114 -0
- package/src/components/table/filter-panel.tsx +407 -0
- package/src/components/table/hooks/use-table-fetch.ts +17 -0
- package/src/components/table/hooks/use-table-query.ts +36 -0
- package/src/components/table/hooks/use-table-state.ts +293 -0
- package/src/components/table/index.ts +35 -0
- package/src/components/table/search-input.tsx +85 -0
- package/src/components/table/sortable-header.tsx +56 -0
- package/src/components/table/table-empty-state.tsx +40 -0
- package/src/components/table/table-no-results.tsx +41 -0
- package/src/components/table/table-pagination.tsx +42 -0
- package/src/components/table/table-skeleton.tsx +54 -0
- package/src/components/table/types.ts +136 -0
- package/src/components/tax-reports/index.ts +1 -0
- package/src/components/tax-reports/kir-export-form.tsx +172 -0
- package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
- package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
- package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
- package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
- package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
- package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
- package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
- package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
- package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
- package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
- package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
- package/src/components/taxes/taxes.hooks.ts +35 -0
- package/src/components/ui/alert-dialog.tsx +61 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +132 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +213 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/chart.tsx +380 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +56 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/dialog.tsx +187 -0
- package/src/components/ui/drawer.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/form.tsx +166 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/loading-spinner.tsx +16 -0
- package/src/components/ui/popover.tsx +108 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +145 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +60 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/sticky-form-footer.tsx +55 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +101 -0
- package/src/components/ui/tabs.tsx +80 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +89 -0
- package/src/components/wl-subscription/index.ts +2 -0
- package/src/components/wl-subscription/locked-feature.tsx +173 -0
- package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
- package/src/frontend.tsx +28 -0
- package/src/generate-schemas.ts +265 -0
- package/src/generated/schemas/advanceinvoice.ts +177 -0
- package/src/generated/schemas/creditnote.ts +187 -0
- package/src/generated/schemas/customer.ts +29 -0
- package/src/generated/schemas/entity.ts +252 -0
- package/src/generated/schemas/estimate.ts +159 -0
- package/src/generated/schemas/furssettings.ts +25 -0
- package/src/generated/schemas/index.ts +24 -0
- package/src/generated/schemas/invoice.ts +167 -0
- package/src/generated/schemas/item.ts +38 -0
- package/src/generated/schemas/payment.ts +44 -0
- package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
- package/src/generated/schemas/previewestimate_body.ts +309 -0
- package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
- package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
- package/src/generated/schemas/renderdocument_body.ts +594 -0
- package/src/generated/schemas/sendemail_body.ts +26 -0
- package/src/generated/schemas/startpdfexport_body.ts +20 -0
- package/src/generated/schemas/tax.ts +48 -0
- package/src/generated/schemas/uploadfile_body.ts +23 -0
- package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
- package/src/generated/schemas/userfurssettings.ts +19 -0
- package/src/hooks/create-resource-hooks.test.ts +483 -0
- package/src/hooks/create-resource-hooks.ts +300 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-duplicate-document.ts +185 -0
- package/src/hooks/use-media-query.tsx +19 -0
- package/src/hooks/use-mobile.ts +39 -0
- package/src/hooks/use-next-document-number.ts +57 -0
- package/src/hooks/use-resource-mutation.ts +118 -0
- package/src/hooks/use-vies-check.ts +130 -0
- package/src/index.css +11 -0
- package/src/index.html +13 -0
- package/src/index.tsx +12 -0
- package/src/lib/auth.ts +4 -0
- package/src/lib/browser-cookies.ts +70 -0
- package/src/lib/constants.ts +287 -0
- package/src/lib/cookies.ts +36 -0
- package/src/lib/schemas/advance-invoice.ts +43 -0
- package/src/lib/schemas/credit-note.ts +32 -0
- package/src/lib/schemas/estimate.ts +31 -0
- package/src/lib/schemas/index.ts +18 -0
- package/src/lib/schemas/invoice.ts +43 -0
- package/src/lib/schemas/shared.ts +79 -0
- package/src/lib/translation.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/entities-context.tsx +41 -0
- package/src/providers/entities-provider.tsx +201 -0
- package/src/providers/form-footer-context.tsx +72 -0
- package/src/providers/sdk-provider.tsx +164 -0
- package/src/providers/white-label-provider.tsx +91 -0
- package/src/providers/wl-subscription-provider.tsx +277 -0
- package/src/utils/string-helpers.ts +111 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import SDK from "@spaceinvoices/js-sdk";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { AUTH_COOKIES } from "@/ui/lib/auth";
|
|
6
|
+
import { flushCookies, getCookie } from "@/ui/lib/browser-cookies";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SDK context type definition
|
|
10
|
+
* Note: SDK is guaranteed to be non-null when accessing this context
|
|
11
|
+
* The provider blocks rendering until SDK is initialized
|
|
12
|
+
*/
|
|
13
|
+
type SDKContextType = {
|
|
14
|
+
sdk: SDK; // Non-null - guaranteed by provider
|
|
15
|
+
isInitialized: boolean;
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
reinitialize: () => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SDK context with default values
|
|
23
|
+
* Note: These defaults are never actually used because the provider
|
|
24
|
+
* blocks rendering until SDK is initialized
|
|
25
|
+
*/
|
|
26
|
+
const SDKContext = createContext<SDKContextType | undefined>(undefined);
|
|
27
|
+
|
|
28
|
+
// Add a name to help with debugging
|
|
29
|
+
SDKContext.displayName = "SDKContext";
|
|
30
|
+
|
|
31
|
+
type SDKProviderProps = {
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
onUnauthorized?: (response: Response) => void;
|
|
34
|
+
fallbackLoading?: ReactNode;
|
|
35
|
+
fallbackError?: (error: Error) => ReactNode;
|
|
36
|
+
fallbackUnauthorized?: ReactNode;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* SDK Provider component
|
|
41
|
+
* Responsible for initializing the SDK and providing it to the application
|
|
42
|
+
*/
|
|
43
|
+
export function SDKProvider({
|
|
44
|
+
children,
|
|
45
|
+
onUnauthorized,
|
|
46
|
+
fallbackLoading = <div className="p-4">Initializing SDK...</div>,
|
|
47
|
+
fallbackError = (error) => <div className="p-4 text-red-500">Error initializing SDK: {error.message}</div>,
|
|
48
|
+
fallbackUnauthorized = <div className="p-4 text-amber-500">No SDK available. Please log in first.</div>,
|
|
49
|
+
}: SDKProviderProps) {
|
|
50
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
51
|
+
const [error, setError] = useState<Error | null>(null);
|
|
52
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
53
|
+
const [sdk, setSdk] = useState<SDK | null>(null);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize the SDK with the current auth token
|
|
57
|
+
*/
|
|
58
|
+
const initializeSDK = async () => {
|
|
59
|
+
setIsLoading(true);
|
|
60
|
+
setError(null);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const token = getCookie(AUTH_COOKIES.TOKEN);
|
|
64
|
+
|
|
65
|
+
if (!token) {
|
|
66
|
+
setIsInitialized(false);
|
|
67
|
+
setSdk(null);
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const basePath = import.meta.env?.VITE_API_URL || import.meta.env?.BUN_PUBLIC_API_URL || undefined;
|
|
73
|
+
const newSDK = new SDK({
|
|
74
|
+
accessToken: token,
|
|
75
|
+
...(basePath && { basePath }),
|
|
76
|
+
onUnauthorized: (response: Response) => {
|
|
77
|
+
flushCookies();
|
|
78
|
+
onUnauthorized?.(response);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
setSdk(newSDK);
|
|
83
|
+
setIsInitialized(true);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
setError(e instanceof Error ? e : new Error("Failed to initialize SDK"));
|
|
86
|
+
setSdk(null);
|
|
87
|
+
setIsInitialized(false);
|
|
88
|
+
} finally {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Initialize SDK on component mount
|
|
94
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: initializeSDK is intentionally omitted - should only run once on mount
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
initializeSDK();
|
|
97
|
+
}, []); // Empty dependency array - only run on mount
|
|
98
|
+
|
|
99
|
+
// IMPORTANT: useMemo must be called BEFORE any conditional returns
|
|
100
|
+
// to satisfy React's Rules of Hooks
|
|
101
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: initializeSDK is stable (doesn't change) and including it would cause unnecessary re-renders
|
|
102
|
+
const value = useMemo(
|
|
103
|
+
() => ({
|
|
104
|
+
sdk: sdk as SDK, // Will be non-null when children render (checked below)
|
|
105
|
+
isInitialized,
|
|
106
|
+
isLoading,
|
|
107
|
+
error,
|
|
108
|
+
reinitialize: initializeSDK,
|
|
109
|
+
}),
|
|
110
|
+
[sdk, isInitialized, isLoading, error], // Don't include initializeSDK - it's stable but causes re-renders
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Render appropriate UI based on SDK state
|
|
114
|
+
// Children only render when SDK is guaranteed non-null
|
|
115
|
+
if (isLoading) {
|
|
116
|
+
return <>{fallbackLoading}</>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (error) {
|
|
120
|
+
return <>{fallbackError(error)}</>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!sdk || !isInitialized) {
|
|
124
|
+
return <>{fallbackUnauthorized}</>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hook to access the SDK context
|
|
132
|
+
* @throws Error if used outside of SDKProvider
|
|
133
|
+
*/
|
|
134
|
+
export function useSDK() {
|
|
135
|
+
const context = useContext(SDKContext);
|
|
136
|
+
|
|
137
|
+
if (context === undefined) {
|
|
138
|
+
throw new Error("useSDK must be used within an SDKProvider");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return context;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Optional hook that returns null if not in SDKProvider
|
|
146
|
+
* Useful for components that may be used with or without SDK context
|
|
147
|
+
*/
|
|
148
|
+
export function useSDKOptional() {
|
|
149
|
+
const context = useContext(SDKContext);
|
|
150
|
+
return context ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get access token from SDK context (helper for WLSubscriptionProvider)
|
|
155
|
+
*/
|
|
156
|
+
export function useAccessToken(): string | null {
|
|
157
|
+
const context = useContext(SDKContext);
|
|
158
|
+
if (!context?.sdk) return null;
|
|
159
|
+
|
|
160
|
+
// Access token is stored in SDK configuration
|
|
161
|
+
// We need to get it from the auth cookie since SDK doesn't expose it directly
|
|
162
|
+
const token = getCookie(AUTH_COOKIES.TOKEN);
|
|
163
|
+
return token ?? null;
|
|
164
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export type WhiteLabelConfig = {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
hiddenFeatures: string[]; // Features hidden from UI (still available via API)
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Default config until API responds (Space Invoices = all features visible)
|
|
11
|
+
const DEFAULT_CONFIG: WhiteLabelConfig = {
|
|
12
|
+
slug: "space-invoices",
|
|
13
|
+
name: "Space Invoices",
|
|
14
|
+
hiddenFeatures: [],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type WhiteLabelContextType = WhiteLabelConfig & {
|
|
18
|
+
/** Check if a feature is visible (not hidden) for this white-label */
|
|
19
|
+
isFeatureVisible: (feature: string) => boolean;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const WhiteLabelContext = createContext<WhiteLabelContextType | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
// Add a name to help with debugging
|
|
26
|
+
WhiteLabelContext.displayName = "WhiteLabelContext";
|
|
27
|
+
|
|
28
|
+
type WhiteLabelProviderProps = {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
/** Optional API base URL (defaults to empty string for same-origin) */
|
|
31
|
+
apiBaseUrl?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* WhiteLabelProvider component
|
|
36
|
+
* Fetches white-label configuration from the API and provides it to the application
|
|
37
|
+
*/
|
|
38
|
+
export function WhiteLabelProvider({ children, apiBaseUrl = "" }: WhiteLabelProviderProps) {
|
|
39
|
+
const [config, setConfig] = useState<WhiteLabelConfig>(DEFAULT_CONFIG);
|
|
40
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const fetchConfig = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(`${apiBaseUrl}/white-labels/current`);
|
|
46
|
+
if (response.ok) {
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
// Map snake_case API response to camelCase internal state
|
|
49
|
+
setConfig({
|
|
50
|
+
slug: data.slug,
|
|
51
|
+
name: data.name,
|
|
52
|
+
hiddenFeatures: data.hidden_features,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Use default config if fetch fails
|
|
57
|
+
} finally {
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
fetchConfig();
|
|
63
|
+
}, [apiBaseUrl]);
|
|
64
|
+
|
|
65
|
+
const isFeatureVisible = (feature: string) => !config.hiddenFeatures.includes(feature);
|
|
66
|
+
|
|
67
|
+
const value = useMemo(
|
|
68
|
+
() => ({
|
|
69
|
+
...config,
|
|
70
|
+
isFeatureVisible,
|
|
71
|
+
isLoading,
|
|
72
|
+
}),
|
|
73
|
+
[config, isLoading, isFeatureVisible],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return <WhiteLabelContext.Provider value={value}>{children}</WhiteLabelContext.Provider>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Hook to access the white-label context
|
|
81
|
+
* @throws Error if used outside of WhiteLabelProvider
|
|
82
|
+
*/
|
|
83
|
+
export function useWhiteLabel() {
|
|
84
|
+
const context = useContext(WhiteLabelContext);
|
|
85
|
+
|
|
86
|
+
if (context === undefined) {
|
|
87
|
+
throw new Error("useWhiteLabel must be used within a WhiteLabelProvider");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return context;
|
|
91
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useEntitiesOptional } from "./entities-context";
|
|
4
|
+
import { useAccessToken } from "./sdk-provider";
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// TYPES
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
export type PlanLimits = {
|
|
11
|
+
documents_per_month: number | null;
|
|
12
|
+
} | null;
|
|
13
|
+
|
|
14
|
+
export type WhiteLabelPlan = {
|
|
15
|
+
id: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
name: string;
|
|
18
|
+
billing_interval: string | null;
|
|
19
|
+
base_price_cents: number | null;
|
|
20
|
+
limits: PlanLimits;
|
|
21
|
+
features: string[];
|
|
22
|
+
is_free: boolean;
|
|
23
|
+
display_order: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type UsageStats = {
|
|
27
|
+
documents_count: number;
|
|
28
|
+
documents_limit: number | null;
|
|
29
|
+
period_start: string;
|
|
30
|
+
period_end: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CurrentSubscription = {
|
|
34
|
+
plan: WhiteLabelPlan;
|
|
35
|
+
status: string;
|
|
36
|
+
billing_interval: string | null;
|
|
37
|
+
current_period_start: string;
|
|
38
|
+
current_period_end: string;
|
|
39
|
+
usage: UsageStats;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Known gated features for Apollo
|
|
43
|
+
export type GatedFeature =
|
|
44
|
+
| "furs"
|
|
45
|
+
| "eslog"
|
|
46
|
+
| "recurring"
|
|
47
|
+
| "email_sending"
|
|
48
|
+
| "custom_templates"
|
|
49
|
+
| "api_access"
|
|
50
|
+
| "webhooks"
|
|
51
|
+
| "priority_support";
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// CONTEXT
|
|
55
|
+
// ============================================
|
|
56
|
+
|
|
57
|
+
type WLSubscriptionContextType = {
|
|
58
|
+
subscription: CurrentSubscription | null;
|
|
59
|
+
plan: WhiteLabelPlan | null;
|
|
60
|
+
usage: UsageStats | null;
|
|
61
|
+
availablePlans: WhiteLabelPlan[];
|
|
62
|
+
isLoading: boolean;
|
|
63
|
+
error: string | null;
|
|
64
|
+
|
|
65
|
+
// Feature/limit checks
|
|
66
|
+
hasFeature: (feature: GatedFeature | string) => boolean;
|
|
67
|
+
isOverLimit: (resource: "documents") => boolean;
|
|
68
|
+
getUsagePercentage: (resource: "documents") => number;
|
|
69
|
+
|
|
70
|
+
// Refresh data
|
|
71
|
+
refresh: () => Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const WLSubscriptionContext = createContext<WLSubscriptionContextType | undefined>(undefined);
|
|
75
|
+
|
|
76
|
+
WLSubscriptionContext.displayName = "WLSubscriptionContext";
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// DEFAULT STATE
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
// Default subscription (unlimited, all features) for non-WL users
|
|
83
|
+
const DEFAULT_SUBSCRIPTION: CurrentSubscription = {
|
|
84
|
+
plan: {
|
|
85
|
+
id: "unlimited",
|
|
86
|
+
slug: "unlimited",
|
|
87
|
+
name: "Unlimited",
|
|
88
|
+
billing_interval: null,
|
|
89
|
+
base_price_cents: null,
|
|
90
|
+
limits: null,
|
|
91
|
+
features: [], // Empty = all features
|
|
92
|
+
is_free: true,
|
|
93
|
+
display_order: 0,
|
|
94
|
+
},
|
|
95
|
+
status: "active",
|
|
96
|
+
billing_interval: null,
|
|
97
|
+
current_period_start: new Date().toISOString(),
|
|
98
|
+
current_period_end: new Date().toISOString(),
|
|
99
|
+
usage: {
|
|
100
|
+
documents_count: 0,
|
|
101
|
+
documents_limit: null,
|
|
102
|
+
period_start: new Date().toISOString(),
|
|
103
|
+
period_end: new Date().toISOString(),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ============================================
|
|
108
|
+
// PROVIDER
|
|
109
|
+
// ============================================
|
|
110
|
+
|
|
111
|
+
type WLSubscriptionProviderProps = {
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
/** API base URL (required for authenticated requests) */
|
|
114
|
+
apiBaseUrl: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* WLSubscriptionProvider component
|
|
119
|
+
* Fetches white-label subscription data and provides limit/feature checks.
|
|
120
|
+
* Must be nested inside EntitiesProvider and SDKProvider.
|
|
121
|
+
*/
|
|
122
|
+
export function WLSubscriptionProvider({ children, apiBaseUrl }: WLSubscriptionProviderProps) {
|
|
123
|
+
// Get entity and access token from existing context
|
|
124
|
+
const entitiesContext = useEntitiesOptional();
|
|
125
|
+
const accessToken = useAccessToken();
|
|
126
|
+
|
|
127
|
+
const entityId = entitiesContext?.activeEntity?.id ?? null;
|
|
128
|
+
const [subscription, setSubscription] = useState<CurrentSubscription>(DEFAULT_SUBSCRIPTION);
|
|
129
|
+
const [availablePlans, setAvailablePlans] = useState<WhiteLabelPlan[]>([]);
|
|
130
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
131
|
+
const [error, setError] = useState<string | null>(null);
|
|
132
|
+
|
|
133
|
+
const fetchSubscription = useCallback(async () => {
|
|
134
|
+
if (!entityId || !accessToken) {
|
|
135
|
+
setSubscription(DEFAULT_SUBSCRIPTION);
|
|
136
|
+
setAvailablePlans([]);
|
|
137
|
+
setIsLoading(false);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
setIsLoading(true);
|
|
143
|
+
setError(null);
|
|
144
|
+
|
|
145
|
+
const headers = {
|
|
146
|
+
Authorization: `Bearer ${accessToken}`,
|
|
147
|
+
"x-entity-id": entityId,
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Fetch current subscription
|
|
152
|
+
const subResponse = await fetch(`${apiBaseUrl}/white-label-subscriptions`, { headers });
|
|
153
|
+
|
|
154
|
+
if (subResponse.ok) {
|
|
155
|
+
const subData = await subResponse.json();
|
|
156
|
+
setSubscription(subData);
|
|
157
|
+
} else if (subResponse.status === 404) {
|
|
158
|
+
// No WL subscription = use default (unlimited)
|
|
159
|
+
setSubscription(DEFAULT_SUBSCRIPTION);
|
|
160
|
+
} else {
|
|
161
|
+
throw new Error(`Failed to fetch subscription: ${subResponse.status}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fetch available plans
|
|
165
|
+
const plansResponse = await fetch(`${apiBaseUrl}/white-label-subscriptions/plans`, {
|
|
166
|
+
headers,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (plansResponse.ok) {
|
|
170
|
+
const plansData = await plansResponse.json();
|
|
171
|
+
setAvailablePlans(plansData.plans || []);
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
setError(err instanceof Error ? err.message : "Failed to fetch subscription");
|
|
175
|
+
setSubscription(DEFAULT_SUBSCRIPTION);
|
|
176
|
+
} finally {
|
|
177
|
+
setIsLoading(false);
|
|
178
|
+
}
|
|
179
|
+
}, [apiBaseUrl, entityId, accessToken]);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
fetchSubscription();
|
|
183
|
+
}, [fetchSubscription]);
|
|
184
|
+
|
|
185
|
+
// Check if feature is available on current plan
|
|
186
|
+
const hasFeature = useCallback(
|
|
187
|
+
(feature: GatedFeature | string): boolean => {
|
|
188
|
+
const plan = subscription.plan;
|
|
189
|
+
|
|
190
|
+
// Empty features array = all features included
|
|
191
|
+
if (plan.features.length === 0) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return plan.features.includes(feature);
|
|
196
|
+
},
|
|
197
|
+
[subscription],
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Check if over document limit
|
|
201
|
+
const isOverLimit = useCallback(
|
|
202
|
+
(resource: "documents"): boolean => {
|
|
203
|
+
if (resource !== "documents") return false;
|
|
204
|
+
|
|
205
|
+
const { usage, plan } = subscription;
|
|
206
|
+
const limit = plan.limits?.documents_per_month;
|
|
207
|
+
|
|
208
|
+
if (limit === null || limit === undefined) {
|
|
209
|
+
return false; // No limit = never over
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return usage.documents_count >= limit;
|
|
213
|
+
},
|
|
214
|
+
[subscription],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Get usage percentage for resource
|
|
218
|
+
const getUsagePercentage = useCallback(
|
|
219
|
+
(resource: "documents"): number => {
|
|
220
|
+
if (resource !== "documents") return 0;
|
|
221
|
+
|
|
222
|
+
const { usage, plan } = subscription;
|
|
223
|
+
const limit = plan.limits?.documents_per_month;
|
|
224
|
+
|
|
225
|
+
if (limit === null || limit === undefined || limit === 0) {
|
|
226
|
+
return 0; // No limit = 0%
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return Math.min(100, Math.round((usage.documents_count / limit) * 100));
|
|
230
|
+
},
|
|
231
|
+
[subscription],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const value = useMemo(
|
|
235
|
+
() => ({
|
|
236
|
+
subscription,
|
|
237
|
+
plan: subscription.plan,
|
|
238
|
+
usage: subscription.usage,
|
|
239
|
+
availablePlans,
|
|
240
|
+
isLoading,
|
|
241
|
+
error,
|
|
242
|
+
hasFeature,
|
|
243
|
+
isOverLimit,
|
|
244
|
+
getUsagePercentage,
|
|
245
|
+
refresh: fetchSubscription,
|
|
246
|
+
}),
|
|
247
|
+
[subscription, availablePlans, isLoading, error, hasFeature, isOverLimit, getUsagePercentage, fetchSubscription],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return <WLSubscriptionContext.Provider value={value}>{children}</WLSubscriptionContext.Provider>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================
|
|
254
|
+
// HOOK
|
|
255
|
+
// ============================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Hook to access the white-label subscription context
|
|
259
|
+
* @throws Error if used outside of WLSubscriptionProvider
|
|
260
|
+
*/
|
|
261
|
+
export function useWLSubscription() {
|
|
262
|
+
const context = useContext(WLSubscriptionContext);
|
|
263
|
+
|
|
264
|
+
if (context === undefined) {
|
|
265
|
+
throw new Error("useWLSubscription must be used within a WLSubscriptionProvider");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return context;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Optional hook that returns null if not in WLSubscriptionProvider
|
|
273
|
+
* Useful for components that may be used with or without subscription context
|
|
274
|
+
*/
|
|
275
|
+
export function useWLSubscriptionOptional() {
|
|
276
|
+
return useContext(WLSubscriptionContext) ?? null;
|
|
277
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Capitalize first letter of string
|
|
7
|
+
* @param str - The string to capitalize
|
|
8
|
+
* @returns The capitalized string
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* capitalize("hello") // "Hello"
|
|
12
|
+
* capitalize("HELLO") // "HELLO"
|
|
13
|
+
*/
|
|
14
|
+
export function capitalize(str: string): string {
|
|
15
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert string to kebab-case
|
|
20
|
+
* @param str - The string to convert
|
|
21
|
+
* @returns The kebab-cased string
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* toKebabCase("helloWorld") // "hello-world"
|
|
25
|
+
* toKebabCase("HelloWorld") // "hello-world"
|
|
26
|
+
*/
|
|
27
|
+
export function toKebabCase(str: string): string {
|
|
28
|
+
return str
|
|
29
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
30
|
+
.replace(/[\s_]+/g, "-")
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert string to PascalCase
|
|
36
|
+
* @param str - The string to convert
|
|
37
|
+
* @returns The PascalCased string
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* toPascalCase("hello world") // "HelloWorld"
|
|
41
|
+
* toPascalCase("hello-world") // "HelloWorld"
|
|
42
|
+
*/
|
|
43
|
+
export function toPascalCase(str: string): string {
|
|
44
|
+
return str
|
|
45
|
+
.split(/[-_\s]+/)
|
|
46
|
+
.map((word) => capitalize(word))
|
|
47
|
+
.join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert string to camelCase
|
|
52
|
+
* @param str - The string to convert
|
|
53
|
+
* @returns The camelCased string
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* toCamelCase("hello world") // "helloWorld"
|
|
57
|
+
* toCamelCase("hello-world") // "helloWorld"
|
|
58
|
+
*/
|
|
59
|
+
export function toCamelCase(str: string): string {
|
|
60
|
+
const pascal = toPascalCase(str);
|
|
61
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert string to Title Case
|
|
66
|
+
* @param str - The string to convert
|
|
67
|
+
* @returns The Title Cased string
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* toTitleCase("hello world") // "Hello World"
|
|
71
|
+
* toTitleCase("hello-world") // "Hello World"
|
|
72
|
+
*/
|
|
73
|
+
export function toTitleCase(str: string): string {
|
|
74
|
+
return str
|
|
75
|
+
.split(/[-_\s]+/)
|
|
76
|
+
.map((word) => capitalize(word))
|
|
77
|
+
.join(" ");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a URL-friendly slug from a string
|
|
82
|
+
* @param str - The string to slugify
|
|
83
|
+
* @returns The slugified string
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* slugify("Hello World!") // "hello-world"
|
|
87
|
+
* slugify("Hello World") // "hello-world"
|
|
88
|
+
*/
|
|
89
|
+
export function slugify(str: string): string {
|
|
90
|
+
return str
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^\w\s-]/g, "")
|
|
93
|
+
.replace(/[\s_]+/g, "-")
|
|
94
|
+
.replace(/^-+|-+$/g, "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Truncate a string to a maximum length
|
|
99
|
+
* @param str - The string to truncate
|
|
100
|
+
* @param maxLength - The maximum length
|
|
101
|
+
* @param suffix - The suffix to append (default: "...")
|
|
102
|
+
* @returns The truncated string
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* truncate("Hello World", 5) // "Hello..."
|
|
106
|
+
* truncate("Hello", 10) // "Hello"
|
|
107
|
+
*/
|
|
108
|
+
export function truncate(str: string, maxLength: number, suffix = "..."): string {
|
|
109
|
+
if (str.length <= maxLength) return str;
|
|
110
|
+
return str.slice(0, maxLength - suffix.length) + suffix;
|
|
111
|
+
}
|