@stacksjs/defaults 0.70.54
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.md +21 -0
- package/README.md +2 -0
- package/package.json +20 -0
- package/resources/assets/fonts/013717e9.woff +0 -0
- package/resources/assets/fonts/03355817.woff2 +0 -0
- package/resources/assets/fonts/0380de7e.woff2 +0 -0
- package/resources/assets/fonts/0ba5d32a.woff +0 -0
- package/resources/assets/fonts/0bd57ddf.woff2 +0 -0
- package/resources/assets/fonts/19de0a60.woff2 +0 -0
- package/resources/assets/fonts/20b87e84.woff +0 -0
- package/resources/assets/fonts/4bbc67e1.woff +0 -0
- package/resources/assets/fonts/576fb0d7.woff +0 -0
- package/resources/assets/fonts/5ea10ee5.woff2 +0 -0
- package/resources/assets/fonts/6413fc0a.woff +0 -0
- package/resources/assets/fonts/72c114cf.woff +0 -0
- package/resources/assets/fonts/73c57e73.woff2 +0 -0
- package/resources/assets/fonts/813dca6c.woff +0 -0
- package/resources/assets/fonts/8a9b0995.woff2 +0 -0
- package/resources/assets/fonts/90b4832f.woff +0 -0
- package/resources/assets/fonts/9394ab85.woff2 +0 -0
- package/resources/assets/fonts/9b3c7d7f.woff2 +0 -0
- package/resources/assets/fonts/Monaco.ttf +0 -0
- package/resources/assets/fonts/b1b36d74.woff +0 -0
- package/resources/assets/fonts/b5d74798.woff2 +0 -0
- package/resources/assets/fonts/b9f81a4d.woff +0 -0
- package/resources/assets/fonts/c9784008.woff2 +0 -0
- package/resources/assets/fonts/cf692227.woff +0 -0
- package/resources/assets/fonts/d1c6ab51.woff2 +0 -0
- package/resources/assets/fonts/e2eb62fc.woff +0 -0
- package/resources/assets/fonts/fe21f8d8.woff2 +0 -0
- package/resources/assets/fonts/nps/CampmateScript-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/NPS_2026-variable.woff2 +0 -0
- package/resources/assets/fonts/nps/RedwoodSerif-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/SequoiaSans-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/Switchback-Regular.woff2 +0 -0
- package/resources/assets/images/app-icon.png +0 -0
- package/resources/assets/images/bg-img.png +0 -0
- package/resources/assets/images/park-ridge.svg +15 -0
- package/resources/assets/images/topography.svg +1 -0
- package/resources/assets/scripts/main.ts +2153 -0
- package/resources/assets/styles/README.md +19 -0
- package/resources/assets/styles/docs/main.css +1 -0
- package/resources/assets/styles/docs/overrides.css +1 -0
- package/resources/assets/styles/docs/vars.css +8 -0
- package/resources/assets/styles/main.css +1419 -0
- package/resources/components/Button.stx +70 -0
- package/resources/components/Buttons/BaseButton.stx +39 -0
- package/resources/components/Buttons/Counter.stx +28 -0
- package/resources/components/Buttons/ToggleDark.stx +11 -0
- package/resources/components/Container.stx +10 -0
- package/resources/components/Dashboard/.gitkeep +0 -0
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +127 -0
- package/resources/components/Dashboard/Auth/Login.stx +212 -0
- package/resources/components/Dashboard/Auth/Register.stx +306 -0
- package/resources/components/Dashboard/Avatar.stx +123 -0
- package/resources/components/Dashboard/Billing/ActivePlan.stx +57 -0
- package/resources/components/Dashboard/Billing/CardBrands.stx +32 -0
- package/resources/components/Dashboard/Billing/CardForm.stx +34 -0
- package/resources/components/Dashboard/Billing/OneTimePayment.stx +28 -0
- package/resources/components/Dashboard/Billing/PaymentForm.stx +33 -0
- package/resources/components/Dashboard/Billing/PaymentMethod.stx +66 -0
- package/resources/components/Dashboard/Billing/PaymentMethodList.stx +52 -0
- package/resources/components/Dashboard/Billing/Plans.stx +136 -0
- package/resources/components/Dashboard/Buttons/AppButton.stx +48 -0
- package/resources/components/Dashboard/Buttons/IconButton.stx +20 -0
- package/resources/components/Dashboard/Card.stx +78 -0
- package/resources/components/Dashboard/Chart.stx +75 -0
- package/resources/components/Dashboard/CodeEditor.stx +41 -0
- package/resources/components/Dashboard/Commerce/BulkActionBar.stx +70 -0
- package/resources/components/Dashboard/Commerce/CategoriesTable.stx +133 -0
- package/resources/components/Dashboard/Commerce/CouponsTable.stx +161 -0
- package/resources/components/Dashboard/Commerce/CustomersTable.stx +142 -0
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +80 -0
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +85 -0
- package/resources/components/Dashboard/Commerce/Delivery/DriversTable.stx +99 -0
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +92 -0
- package/resources/components/Dashboard/Commerce/Delivery/LicenseTemplatesTable.stx +88 -0
- package/resources/components/Dashboard/Commerce/Delivery/Pagination.stx +122 -0
- package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +23 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +80 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +59 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +71 -0
- package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +36 -0
- package/resources/components/Dashboard/Commerce/Forms/CouponsForm.stx +191 -0
- package/resources/components/Dashboard/Commerce/OrdersTable.stx +131 -0
- package/resources/components/Dashboard/Commerce/ProductsTable.stx +170 -0
- package/resources/components/Dashboard/CreateRecordModal.stx +363 -0
- package/resources/components/Dashboard/DashboardLayout.stx +155 -0
- package/resources/components/Dashboard/Deployments/ActivityFeed.stx +54 -0
- package/resources/components/Dashboard/Deployments/DeployScript.stx +23 -0
- package/resources/components/Dashboard/Deployments/DeploymentHistory.stx +97 -0
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +94 -0
- package/resources/components/Dashboard/Deployments/LiveTerminalOutput.stx +18 -0
- package/resources/components/Dashboard/Elements/Dropdown.stx +57 -0
- package/resources/components/Dashboard/Email/EmailActivityChart.stx +111 -0
- package/resources/components/Dashboard/Email/EmailCompose.stx +72 -0
- package/resources/components/Dashboard/Email/EmailDetail.stx +105 -0
- package/resources/components/Dashboard/Email/EmailList.stx +117 -0
- package/resources/components/Dashboard/Email/EmailSearch.stx +40 -0
- package/resources/components/Dashboard/Email/EmailSidebar.stx +133 -0
- package/resources/components/Dashboard/GlobalSearch.stx +124 -0
- package/resources/components/Dashboard/MobileSidebar.stx +47 -0
- package/resources/components/Dashboard/Modals/BaseModal.stx +50 -0
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +112 -0
- package/resources/components/Dashboard/Modals/Popups/Toast.stx +70 -0
- package/resources/components/Dashboard/Modals/ToastWrapper.stx +35 -0
- package/resources/components/Dashboard/Monitoring/ErrorDetailModal.stx +228 -0
- package/resources/components/Dashboard/Monitoring/ErrorsTable.stx +184 -0
- package/resources/components/Dashboard/Navbar.stx +194 -0
- package/resources/components/Dashboard/NavbarModern.stx +188 -0
- package/resources/components/Dashboard/NotificationErrorModal.stx +105 -0
- package/resources/components/Dashboard/NotificationStatusBadge.stx +20 -0
- package/resources/components/Dashboard/Pagination.stx +90 -0
- package/resources/components/Dashboard/Queue/QueueTable.stx +127 -0
- package/resources/components/Dashboard/Settings/Forms/AIForm.stx +126 -0
- package/resources/components/Dashboard/Settings/Forms/AnalyticsForm.stx +52 -0
- package/resources/components/Dashboard/Settings/Forms/AppForm.stx +157 -0
- package/resources/components/Dashboard/Settings/Forms/CLIForm.stx +66 -0
- package/resources/components/Dashboard/Settings/Forms/CacheForm.stx +253 -0
- package/resources/components/Dashboard/Settings/Forms/CloudForm.stx +441 -0
- package/resources/components/Dashboard/Settings/Forms/DNSForm.stx +307 -0
- package/resources/components/Dashboard/Settings/Forms/DatabaseForm.stx +168 -0
- package/resources/components/Dashboard/Settings/Forms/EmailForm.stx +100 -0
- package/resources/components/Dashboard/Settings/Forms/FileSystemForm.stx +259 -0
- package/resources/components/Dashboard/Settings/Forms/HashingForm.stx +119 -0
- package/resources/components/Dashboard/Settings/Forms/LibraryForm.stx +389 -0
- package/resources/components/Dashboard/Settings/Forms/QueueForm.stx +219 -0
- package/resources/components/Dashboard/Settings/Forms/SearchEngineForm.stx +48 -0
- package/resources/components/Dashboard/Settings/Forms/SecurityForm.stx +143 -0
- package/resources/components/Dashboard/Settings/Forms/ServicesForm.stx +194 -0
- package/resources/components/Dashboard/Settings/Forms/StorageForm.stx +191 -0
- package/resources/components/Dashboard/Settings/Forms/TeamsForm.stx +33 -0
- package/resources/components/Dashboard/Settings/SettingsFormManager.stx +181 -0
- package/resources/components/Dashboard/SettingsHeader.stx +88 -0
- package/resources/components/Dashboard/SettingsSidebar.stx +234 -0
- package/resources/components/Dashboard/Sidebar.stx +23 -0
- package/resources/components/Dashboard/SidebarModern.stx +249 -0
- package/resources/components/Dashboard/Skeleton/LoadingCard.stx +15 -0
- package/resources/components/Dashboard/Skeleton/LoadingDetails.stx +26 -0
- package/resources/components/Dashboard/Table.stx +82 -0
- package/resources/components/Dashboard/TerminalContainer.stx +97 -0
- package/resources/components/Dashboard/Tooltip.stx +166 -0
- package/resources/components/Dashboard/Transaction/index.stx +88 -0
- package/resources/components/Dashboard/UI/ActivityTable.stx +54 -0
- package/resources/components/Dashboard/UI/Avatar.stx +102 -0
- package/resources/components/Dashboard/UI/Badge.stx +66 -0
- package/resources/components/Dashboard/UI/Button.stx +73 -0
- package/resources/components/Dashboard/UI/Card.stx +51 -0
- package/resources/components/Dashboard/UI/ChartCard.stx +36 -0
- package/resources/components/Dashboard/UI/Checkbox.stx +74 -0
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +74 -0
- package/resources/components/Dashboard/UI/DataTable.stx +45 -0
- package/resources/components/Dashboard/UI/Dropdown.stx +72 -0
- package/resources/components/Dashboard/UI/DropdownItem.stx +80 -0
- package/resources/components/Dashboard/UI/EmptyState.stx +50 -0
- package/resources/components/Dashboard/UI/FilterBar.stx +48 -0
- package/resources/components/Dashboard/UI/Input.stx +77 -0
- package/resources/components/Dashboard/UI/Modal.stx +75 -0
- package/resources/components/Dashboard/UI/PageHeader.stx +69 -0
- package/resources/components/Dashboard/UI/PageLayout.stx +84 -0
- package/resources/components/Dashboard/UI/Pagination.stx +126 -0
- package/resources/components/Dashboard/UI/QuickLinks.stx +31 -0
- package/resources/components/Dashboard/UI/RouterLink.stx +31 -0
- package/resources/components/Dashboard/UI/Select.stx +84 -0
- package/resources/components/Dashboard/UI/ServiceHealth.stx +38 -0
- package/resources/components/Dashboard/UI/StatsCard.stx +79 -0
- package/resources/components/Dashboard/UI/Table.stx +143 -0
- package/resources/components/Dashboard/UI/Tabs.stx +45 -0
- package/resources/components/Dashboard/UI/Textarea.stx +93 -0
- package/resources/components/Dashboard/UI/Toggle.stx +84 -0
- package/resources/components/Dashboard/UI/WindowControls.stx +135 -0
- package/resources/components/Dashboard/Widget.stx +89 -0
- package/resources/components/Dashboard/WindowControls.stx +119 -0
- package/resources/components/Dashboard/index.ts +106 -0
- package/resources/components/Docs/Demo/ComboboxCode.md +114 -0
- package/resources/components/Docs/Demo/ComboboxDemo.stx +91 -0
- package/resources/components/Docs/Demo/DialogCode.md +42 -0
- package/resources/components/Docs/Demo/DialogDemo.stx +76 -0
- package/resources/components/Docs/Demo/DocsPlayground.stx +39 -0
- package/resources/components/Docs/Demo/DropdownCode.md +109 -0
- package/resources/components/Docs/Demo/DropdownDemo.stx +82 -0
- package/resources/components/Docs/Demo/Hero.stx +100 -0
- package/resources/components/Docs/Demo/ListboxCode.md +87 -0
- package/resources/components/Docs/Demo/ListboxDemo.stx +59 -0
- package/resources/components/Docs/Demo/NotificationCode.md +173 -0
- package/resources/components/Docs/Demo/NotificationDemo.stx +99 -0
- package/resources/components/Docs/Demo/PopoverCode.md +161 -0
- package/resources/components/Docs/Demo/PopoverDemo.stx +96 -0
- package/resources/components/Docs/Demo/RadioGroupCode.md +98 -0
- package/resources/components/Docs/Demo/RadioGroupDemo.stx +53 -0
- package/resources/components/Docs/Demo/StepperCode.md +216 -0
- package/resources/components/Docs/Demo/StepperDemo.stx +137 -0
- package/resources/components/Docs/Demo/SwitchCode.md +24 -0
- package/resources/components/Docs/Demo/SwitchDemo.stx +28 -0
- package/resources/components/Docs/Home.stx +7 -0
- package/resources/components/Docs/HomeContributors.stx +23 -0
- package/resources/components/Docs/HomeSponsors.stx +17 -0
- package/resources/components/Docs/HomeTeam.stx +18 -0
- package/resources/components/Docs/TeamMember.stx +79 -0
- package/resources/components/Email/EmailButton.stx +32 -0
- package/resources/components/Email/EmailDivider.stx +20 -0
- package/resources/components/Email/EmailImage.stx +24 -0
- package/resources/components/Email/EmailLayout.stx +55 -0
- package/resources/components/Email/EmailLink.stx +22 -0
- package/resources/components/Email/EmailSection.stx +20 -0
- package/resources/components/Email/EmailText.stx +30 -0
- package/resources/components/Forum/ForumBreadcrumb.stx +38 -0
- package/resources/components/Forum/ForumCategory.stx +16 -0
- package/resources/components/Forum/ForumCategoryItem.stx +46 -0
- package/resources/components/Forum/ForumLayout.stx +36 -0
- package/resources/components/Forum/ForumPagination.stx +39 -0
- package/resources/components/Forum/ForumPost.stx +71 -0
- package/resources/components/Forum/ForumReplyForm.stx +76 -0
- package/resources/components/Forum/ForumSidebar.stx +86 -0
- package/resources/components/Forum/ForumTopicItem.stx +77 -0
- package/resources/components/Forum/ForumTopicList.stx +23 -0
- package/resources/components/HelloWorld.stx +253 -0
- package/resources/components/Logo.stx +10 -0
- package/resources/components/Marketing/CallToAction.stx +30 -0
- package/resources/components/Marketing/ComingSoon.stx +155 -0
- package/resources/components/Marketing/Faqs.stx +99 -0
- package/resources/components/Marketing/Feature.stx +32 -0
- package/resources/components/Marketing/Fields.stx +28 -0
- package/resources/components/Marketing/Footer.stx +33 -0
- package/resources/components/Marketing/Header.stx +51 -0
- package/resources/components/Marketing/Hero.stx +46 -0
- package/resources/components/Marketing/Layout.stx +26 -0
- package/resources/components/Marketing/NavLink.stx +13 -0
- package/resources/components/Marketing/Plan.stx +43 -0
- package/resources/components/Marketing/Pricing.stx +120 -0
- package/resources/components/Marketing/PrimaryFeatures.stx +80 -0
- package/resources/components/Marketing/PrimaryFeatures2.stx +98 -0
- package/resources/components/Marketing/SecondaryFeatures.stx +80 -0
- package/resources/components/Marketing/Testimonials.stx +120 -0
- package/resources/components/Pagination.stx +149 -0
- package/resources/components/Playground.stx +9 -0
- package/resources/components/README.md +87 -0
- package/resources/components/Storefront/AnnouncementBar.stx +17 -0
- package/resources/components/Storefront/CartDrawer.stx +227 -0
- package/resources/components/Storefront/CheckoutSteps.stx +50 -0
- package/resources/components/Storefront/Footer.stx +81 -0
- package/resources/components/Storefront/Header.stx +66 -0
- package/resources/components/Storefront/OrderSummary.stx +79 -0
- package/resources/components/Storefront/ProductCard.stx +63 -0
- package/resources/components/Storefront/ProductGallery.stx +88 -0
- package/resources/components/Storefront/QuantitySelector.stx +97 -0
- package/resources/components/index.ts +13 -0
- package/resources/emails/email-verification.stx +46 -0
- package/resources/emails/layouts/base.html +113 -0
- package/resources/emails/password-changed.stx +55 -0
- package/resources/emails/password-reset.stx +43 -0
- package/resources/emails/subscription-confirmation.stx +99 -0
- package/resources/emails/welcome.stx +48 -0
- package/resources/functions/README.md +28 -0
- package/resources/functions/counter.ts +13 -0
- package/resources/functions/dark.ts +4 -0
- package/resources/functions/dashboard/analytics.ts +238 -0
- package/resources/functions/dashboard/config-io.ts +316 -0
- package/resources/functions/dashboard/data.ts +375 -0
- package/resources/functions/dashboard/library.ts +250 -0
- package/resources/functions/dashboard/sidebar.ts +537 -0
- package/resources/functions/dashboard/system.ts +150 -0
- package/resources/layouts/403.stx +188 -0
- package/resources/layouts/404.stx +34 -0
- package/resources/layouts/README.md +14 -0
- package/resources/layouts/dashboard/default.stx +20 -0
- package/resources/layouts/dashboard/guest.stx +10 -0
- package/resources/layouts/default.stx +3 -0
- package/resources/layouts/maintenance.stx +11 -0
- package/resources/layouts/storefront.stx +73 -0
- package/resources/layouts/system-tray/default.stx +3 -0
- package/resources/modules/.gitkeep +0 -0
- package/resources/plugins/.gitkeep +0 -0
- package/resources/plugins/preloader.ts +308 -0
- package/resources/types/README.md +3 -0
- package/resources/views/cart.stx +149 -0
- package/resources/views/checkout/contact.stx +101 -0
- package/resources/views/checkout/payment.stx +114 -0
- package/resources/views/checkout/shipping.stx +120 -0
- package/resources/views/coming-soon.stx +9 -0
- package/resources/views/dashboard/custom-page.stx +45 -0
- package/resources/views/dashboard/index.stx +24 -0
- package/resources/views/emails/welcome.stx +0 -0
- package/resources/views/errors/tester.stx +26 -0
- package/resources/views/index.stx +2865 -0
- package/resources/views/orders/[id].stx +130 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Password-changed notification — sent after the user's password is
|
|
3
|
+
// updated. Includes a security warning block for the "wasn't me"
|
|
4
|
+
// case. Uses bundled <EmailLayout>/<EmailButton>/<EmailDivider>
|
|
5
|
+
// components (stacksjs/stacks#1901).
|
|
6
|
+
const appName = props.appName || 'Stacks'
|
|
7
|
+
const changedAt = props.changedAt || new Date().toISOString()
|
|
8
|
+
const supportEmail = props.supportEmail || ''
|
|
9
|
+
const primaryColor = props.primaryColor || '#3451b2'
|
|
10
|
+
const subject = props.subject || `Your ${appName} password was changed`
|
|
11
|
+
const year = props.year || new Date().getFullYear()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<EmailLayout title="{{ subject }}">
|
|
15
|
+
<template #header>
|
|
16
|
+
<tr>
|
|
17
|
+
<td style="background-color:{{ primaryColor }};padding:30px 40px;text-align:center;">
|
|
18
|
+
<h1 style="margin:0;color:#ffffff;font-size:24px;font-weight:600;">{{ appName }}</h1>
|
|
19
|
+
</td>
|
|
20
|
+
</tr>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<EmailText size="heading">Password changed successfully</EmailText>
|
|
24
|
+
<EmailText>Your <strong>{{ appName }}</strong> account password was successfully changed on:</EmailText>
|
|
25
|
+
|
|
26
|
+
<p style="margin:0 0 20px 0;padding:15px;background-color:#f3f4f6;border-radius:6px;color:#111827;font-size:16px;font-weight:500;text-align:center;">
|
|
27
|
+
{{ changedAt }}
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<EmailText>If you made this change, you can safely ignore this email.</EmailText>
|
|
31
|
+
|
|
32
|
+
<div style="padding:20px;background-color:#fef2f2;border:1px solid #fecaca;border-radius:6px;margin:20px 0;">
|
|
33
|
+
<p style="margin:0;color:#991b1b;font-size:14px;font-weight:600;">Didn't make this change?</p>
|
|
34
|
+
<p style="margin:10px 0 0 0;color:#7f1d1d;font-size:14px;">
|
|
35
|
+
If you did not change your password, your account may have been compromised. Please contact support immediately@if(supportEmail) at <a href="mailto:{{ supportEmail }}" style="color:#991b1b;">{{ supportEmail }}</a>@endif.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<EmailText size="lg" color="#111827">Security recommendations</EmailText>
|
|
40
|
+
<ul style="margin:0;padding:0 0 0 20px;color:#4b5563;font-size:14px;">
|
|
41
|
+
<li style="margin-bottom:8px;">Use a strong, unique password</li>
|
|
42
|
+
<li style="margin-bottom:8px;">Enable two-factor authentication if available</li>
|
|
43
|
+
<li style="margin-bottom:8px;">Review your recent account activity</li>
|
|
44
|
+
<li style="margin-bottom:8px;">Never share your password with anyone</li>
|
|
45
|
+
</ul>
|
|
46
|
+
|
|
47
|
+
<template #footer>
|
|
48
|
+
<tr>
|
|
49
|
+
<td style="padding:20px 40px;background-color:#f9fafb;border-top:1px solid #e5e7eb;">
|
|
50
|
+
<p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
|
|
51
|
+
<p style="margin:10px 0 0 0;color:#9ca3af;font-size:11px;text-align:center;">© {{ year }} {{ appName }}. All rights reserved.</p>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</template>
|
|
55
|
+
</EmailLayout>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Password reset email. Uses bundled <EmailLayout>/<EmailButton>
|
|
3
|
+
// components (stacksjs/stacks#1901).
|
|
4
|
+
const appName = props.appName || 'Stacks'
|
|
5
|
+
const resetUrl = props.resetUrl || '#'
|
|
6
|
+
const expireMinutes = props.expireMinutes || 60
|
|
7
|
+
const primaryColor = props.primaryColor || '#3451b2'
|
|
8
|
+
const subject = props.subject || `Reset your ${appName} password`
|
|
9
|
+
const year = props.year || new Date().getFullYear()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<EmailLayout title="{{ subject }}">
|
|
13
|
+
<template #header>
|
|
14
|
+
<tr>
|
|
15
|
+
<td style="background-color:{{ primaryColor }};padding:30px 40px;text-align:center;">
|
|
16
|
+
<h1 style="margin:0;color:#ffffff;font-size:24px;font-weight:600;">{{ appName }}</h1>
|
|
17
|
+
</td>
|
|
18
|
+
</tr>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<EmailText size="heading">Reset your password</EmailText>
|
|
22
|
+
<EmailText>You recently requested to reset your password for your <strong>{{ appName }}</strong> account. Click the button below to reset it.</EmailText>
|
|
23
|
+
|
|
24
|
+
<EmailButton href="{{ resetUrl }}" bg="{{ primaryColor }}">Reset Password</EmailButton>
|
|
25
|
+
|
|
26
|
+
<EmailText size="sm" color="#6b7280">This password reset link will expire in <strong>{{ expireMinutes }} minutes</strong>.</EmailText>
|
|
27
|
+
<EmailText size="sm" color="#6b7280">If you did not request a password reset, please ignore this email or contact support if you have concerns about your account security.</EmailText>
|
|
28
|
+
|
|
29
|
+
<EmailDivider />
|
|
30
|
+
<EmailText size="sm" color="#9ca3af" spacing="8px">If the button above doesn't work, copy and paste the following URL into your browser:</EmailText>
|
|
31
|
+
<p style="margin:0;word-break:break-all;">
|
|
32
|
+
<EmailLink href="{{ resetUrl }}" color="{{ primaryColor }}">{{ resetUrl }}</EmailLink>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<template #footer>
|
|
36
|
+
<tr>
|
|
37
|
+
<td style="padding:20px 40px;background-color:#f9fafb;border-top:1px solid #e5e7eb;">
|
|
38
|
+
<p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
|
|
39
|
+
<p style="margin:10px 0 0 0;color:#9ca3af;font-size:11px;text-align:center;">© {{ year }} {{ appName }}. All rights reserved.</p>
|
|
40
|
+
</td>
|
|
41
|
+
</tr>
|
|
42
|
+
</template>
|
|
43
|
+
</EmailLayout>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
const email = props.email || ''
|
|
3
|
+
const unsubscribeUrl = props.unsubscribeUrl || '#'
|
|
4
|
+
const appName = props.appName || 'Stacks'
|
|
5
|
+
const appUrl = props.appUrl || 'https://stacksjs.com'
|
|
6
|
+
const primaryColor = props.primaryColor || '#3451b2'
|
|
7
|
+
const year = props.year || new Date().getFullYear()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!DOCTYPE html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
16
|
+
<title>Welcome to {{ appName }}</title>
|
|
17
|
+
</head>
|
|
18
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #0e0e0e; line-height: 1.6; color: #d4d4d4;">
|
|
19
|
+
<table role="presentation" cellpadding="0" cellspacing="0" style="width: 100%; border-collapse: collapse;">
|
|
20
|
+
<tr>
|
|
21
|
+
<td style="padding: 40px 20px;">
|
|
22
|
+
<table role="presentation" cellpadding="0" cellspacing="0" style="max-width: 600px; margin: 0 auto;">
|
|
23
|
+
|
|
24
|
+
<!-- Logo -->
|
|
25
|
+
<tr>
|
|
26
|
+
<td style="text-align: center; padding-bottom: 32px;">
|
|
27
|
+
<h1 style="color: #ececec; font-size: 28px; font-weight: 600; margin: 0;">{{ appName }}</h1>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
|
|
31
|
+
<!-- Main Content -->
|
|
32
|
+
<tr>
|
|
33
|
+
<td style="background-color: #1a1a1a; border-radius: 12px; padding: 40px;">
|
|
34
|
+
|
|
35
|
+
<h2 style="color: #ececec; font-size: 22px; font-weight: 600; margin: 0 0 16px;">Thanks for subscribing!</h2>
|
|
36
|
+
|
|
37
|
+
<p style="color: #a0a0a0; font-size: 16px; line-height: 1.6; margin: 0 0 16px;">
|
|
38
|
+
You're now on the list. We'll keep you in the loop on new releases, features, and framework updates.
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p style="color: #a0a0a0; font-size: 16px; line-height: 1.6; margin: 0 0 24px;">
|
|
42
|
+
{{ appName }} is a rapid application development framework that helps you build type-safe web apps, APIs, and cloud infrastructure with ease.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<!-- CTA Button -->
|
|
46
|
+
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 0 24px;">
|
|
47
|
+
<tr>
|
|
48
|
+
<td>
|
|
49
|
+
<a href="{{ appUrl }}" style="display: inline-block; background-color: {{ primaryColor }}; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 8px; font-size: 14px; font-weight: 500;">
|
|
50
|
+
Visit {{ appName }}
|
|
51
|
+
</a>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</table>
|
|
55
|
+
|
|
56
|
+
<!-- Divider -->
|
|
57
|
+
<table role="presentation" cellpadding="0" cellspacing="0" style="width: 100%; margin: 8px 0 24px;">
|
|
58
|
+
<tr>
|
|
59
|
+
<td style="border-top: 1px solid #2a2a2a;"></td>
|
|
60
|
+
</tr>
|
|
61
|
+
</table>
|
|
62
|
+
|
|
63
|
+
<!-- Links -->
|
|
64
|
+
<table role="presentation" cellpadding="0" cellspacing="0" style="width: 100%;">
|
|
65
|
+
<tr>
|
|
66
|
+
<td style="color: #666; font-size: 13px;">
|
|
67
|
+
<a href="https://docs.stacksjs.com" style="color: #888; text-decoration: none;">Docs</a>
|
|
68
|
+
·
|
|
69
|
+
<a href="https://github.com/stacksjs/stacks" style="color: #888; text-decoration: none;">GitHub</a>
|
|
70
|
+
·
|
|
71
|
+
<a href="https://discord.gg/stacksjs" style="color: #888; text-decoration: none;">Discord</a>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
</table>
|
|
75
|
+
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
|
|
79
|
+
<!-- Footer -->
|
|
80
|
+
<tr>
|
|
81
|
+
<td style="text-align: center; padding-top: 32px;">
|
|
82
|
+
<p style="color: #555; font-size: 13px; margin: 0 0 8px;">
|
|
83
|
+
You received this email because {{ email }} subscribed at {{ appUrl }}.
|
|
84
|
+
</p>
|
|
85
|
+
<p style="margin: 0;">
|
|
86
|
+
<a href="{{ unsubscribeUrl }}" style="color: #666; font-size: 12px; text-decoration: underline;">Unsubscribe</a>
|
|
87
|
+
</p>
|
|
88
|
+
<p style="color: #444; font-size: 11px; margin: 12px 0 0;">
|
|
89
|
+
© {{ year }} {{ appName }}. All rights reserved.
|
|
90
|
+
</p>
|
|
91
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
|
|
94
|
+
</table>
|
|
95
|
+
</td>
|
|
96
|
+
</tr>
|
|
97
|
+
</table>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Welcome email — sent after a user signs up. Uses the bundled
|
|
3
|
+
// <EmailLayout> + <EmailSection> + ... component library
|
|
4
|
+
// (stacksjs/stacks#1901). All copy / colour can be overridden by
|
|
5
|
+
// passing the matching prop to the Mailable.
|
|
6
|
+
const appName = props.appName || 'Stacks'
|
|
7
|
+
const appUrl = props.appUrl || 'https://stacksjs.com'
|
|
8
|
+
const userName = props.userName || 'there'
|
|
9
|
+
const primaryColor = props.primaryColor || '#3451b2'
|
|
10
|
+
const subject = props.subject || `Welcome to ${appName}`
|
|
11
|
+
const year = props.year || new Date().getFullYear()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<EmailLayout title="{{ subject }}">
|
|
15
|
+
<template #header>
|
|
16
|
+
<tr>
|
|
17
|
+
<td style="background-color:{{ primaryColor }};padding:30px 40px;text-align:center;">
|
|
18
|
+
<h1 style="margin:0;color:#ffffff;font-size:24px;font-weight:600;">{{ appName }}</h1>
|
|
19
|
+
</td>
|
|
20
|
+
</tr>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<EmailText size="heading">Welcome to {{ appName }}!</EmailText>
|
|
24
|
+
<EmailText>Hi {{ userName }},</EmailText>
|
|
25
|
+
<EmailText>Thank you for signing up — we're excited to have you on board.</EmailText>
|
|
26
|
+
<EmailText>Your account has been created successfully. You can sign in and start exploring everything {{ appName }} has to offer.</EmailText>
|
|
27
|
+
|
|
28
|
+
<EmailButton href="{{ appUrl }}" bg="{{ primaryColor }}">Get Started</EmailButton>
|
|
29
|
+
|
|
30
|
+
<EmailText size="lg" color="#111827">What's next?</EmailText>
|
|
31
|
+
<ul style="margin:0;padding:0 0 0 20px;color:#4b5563;font-size:14px;">
|
|
32
|
+
<li style="margin-bottom:8px;">Complete your profile</li>
|
|
33
|
+
<li style="margin-bottom:8px;">Explore the dashboard</li>
|
|
34
|
+
<li style="margin-bottom:8px;">Check out our documentation</li>
|
|
35
|
+
</ul>
|
|
36
|
+
|
|
37
|
+
<EmailDivider />
|
|
38
|
+
<EmailText size="sm" color="#6b7280" spacing="0">If you have any questions, reach out to support — we're here to help.</EmailText>
|
|
39
|
+
|
|
40
|
+
<template #footer>
|
|
41
|
+
<tr>
|
|
42
|
+
<td style="padding:20px 40px;background-color:#f9fafb;border-top:1px solid #e5e7eb;">
|
|
43
|
+
<p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
|
|
44
|
+
<p style="margin:10px 0 0 0;color:#9ca3af;font-size:11px;text-align:center;">© {{ year }} {{ appName }}. All rights reserved.</p>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</template>
|
|
48
|
+
</EmailLayout>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# The Functions Folder
|
|
2
|
+
|
|
3
|
+
You may use functions to share on npm or have them run on a server behind one of your `../routes`.
|
|
4
|
+
|
|
5
|
+
To learn more about Stacks Functions, check out the [documentation](https://stacksjs.com).
|
|
6
|
+
|
|
7
|
+
> **Note**
|
|
8
|
+
> If your project does not require any functions, you can remove this folder._
|
|
9
|
+
|
|
10
|
+
## 🚜 Contributing
|
|
11
|
+
|
|
12
|
+
Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
|
|
13
|
+
|
|
14
|
+
## 🏝 Community
|
|
15
|
+
|
|
16
|
+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
17
|
+
|
|
18
|
+
[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)
|
|
19
|
+
|
|
20
|
+
For casual chit-chat with others using this package:
|
|
21
|
+
|
|
22
|
+
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
|
|
23
|
+
|
|
24
|
+
## 📄 License
|
|
25
|
+
|
|
26
|
+
The MIT License (MIT). Please see [LICENSE](../LICENSE.md) for more information.
|
|
27
|
+
|
|
28
|
+
Made with 💙
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// reactive state
|
|
2
|
+
const count = ref(0)
|
|
3
|
+
const now = useDateFormat(useNow(), 'YYYY-MM-DD HH:mm:ss')
|
|
4
|
+
|
|
5
|
+
// functions that mutate state and trigger updates
|
|
6
|
+
function increment() {
|
|
7
|
+
// eslint-disable-next-line no-console
|
|
8
|
+
console.log('increment() was last run:', now)
|
|
9
|
+
|
|
10
|
+
count.value++
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { count, increment }
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregator helpers for the Analytics dashboard pages. Each function
|
|
3
|
+
* pulls from the seeded `Activity` / `Request` / `Order` / `Subscriber`
|
|
4
|
+
* tables and reduces to the chart-ready shape the page expects.
|
|
5
|
+
*
|
|
6
|
+
* No model is required to exist — the helpers fall back to empty data
|
|
7
|
+
* when the table or column is missing, so analytics pages still render
|
|
8
|
+
* without errors on an unseeded database.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { countBy, groupByDay, loadModel, safeAll, safeGet, sumByDay, topN } from './data'
|
|
12
|
+
|
|
13
|
+
export interface WebMetrics {
|
|
14
|
+
totalRequests: number
|
|
15
|
+
uniqueVisitors: number
|
|
16
|
+
avgResponseTime: number
|
|
17
|
+
errorRate: number
|
|
18
|
+
perDay: Array<{ date: string, count: number }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Site-traffic aggregations from the `Request` model. Unique visitors
|
|
23
|
+
* are counted by distinct IP; response time / error rate read the
|
|
24
|
+
* `duration_ms` and `status_code` columns the seeder populates.
|
|
25
|
+
*/
|
|
26
|
+
export async function getWebMetrics(days = 30): Promise<WebMetrics> {
|
|
27
|
+
const Request = await loadModel('Request')
|
|
28
|
+
const rows = await safeAll(Request)
|
|
29
|
+
const ips = new Set<string>()
|
|
30
|
+
let totalDuration = 0
|
|
31
|
+
let durationSamples = 0
|
|
32
|
+
let errors = 0
|
|
33
|
+
for (const r of rows) {
|
|
34
|
+
const ip = String(safeGet(r, 'ip_address', '') || safeGet(r, 'ip', ''))
|
|
35
|
+
if (ip) ips.add(ip)
|
|
36
|
+
const dur = Number(safeGet(r, 'duration_ms', 0)) || Number(safeGet(r, 'duration', 0))
|
|
37
|
+
if (dur > 0) {
|
|
38
|
+
totalDuration += dur
|
|
39
|
+
durationSamples++
|
|
40
|
+
}
|
|
41
|
+
const status = Number(safeGet(r, 'status_code', 0)) || Number(safeGet(r, 'status', 0))
|
|
42
|
+
if (status >= 400) errors++
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
totalRequests: rows.length,
|
|
46
|
+
uniqueVisitors: ips.size,
|
|
47
|
+
avgResponseTime: durationSamples > 0 ? Math.round(totalDuration / durationSamples) : 0,
|
|
48
|
+
errorRate: rows.length > 0 ? Math.round((errors / rows.length) * 1000) / 10 : 0,
|
|
49
|
+
perDay: groupByDay(rows, 'created_at', days),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Top referrer hosts. */
|
|
54
|
+
export async function getTopReferrers(n = 10): Promise<Array<{ value: string, count: number }>> {
|
|
55
|
+
const Request = await loadModel('Request')
|
|
56
|
+
const rows = await safeAll(Request)
|
|
57
|
+
return topN(rows, 'referrer', n)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Visitor breakdown by user-agent family (best-effort). */
|
|
61
|
+
export async function getBrowserBreakdown(): Promise<Array<{ value: string, count: number }>> {
|
|
62
|
+
const Request = await loadModel('Request')
|
|
63
|
+
const rows = await safeAll(Request)
|
|
64
|
+
const counts: Record<string, number> = {}
|
|
65
|
+
for (const r of rows) {
|
|
66
|
+
const ua = String(safeGet(r, 'user_agent', '') || safeGet(r, 'browser', ''))
|
|
67
|
+
const fam = parseBrowser(ua)
|
|
68
|
+
counts[fam] = (counts[fam] || 0) + 1
|
|
69
|
+
}
|
|
70
|
+
return Object.entries(counts)
|
|
71
|
+
.map(([value, count]) => ({ value, count }))
|
|
72
|
+
.sort((a, b) => b.count - a.count)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Visitor breakdown by device type (mobile/tablet/desktop). */
|
|
76
|
+
export async function getDeviceBreakdown(): Promise<Array<{ value: string, count: number }>> {
|
|
77
|
+
const Request = await loadModel('Request')
|
|
78
|
+
const rows = await safeAll(Request)
|
|
79
|
+
const counts: Record<string, number> = { desktop: 0, mobile: 0, tablet: 0, other: 0 }
|
|
80
|
+
for (const r of rows) {
|
|
81
|
+
const ua = String(safeGet(r, 'user_agent', '') || safeGet(r, 'device', '')).toLowerCase()
|
|
82
|
+
if (/tablet|ipad/.test(ua)) counts.tablet++
|
|
83
|
+
else if (/mobile|iphone|android/.test(ua)) counts.mobile++
|
|
84
|
+
else if (ua) counts.desktop++
|
|
85
|
+
else counts.other++
|
|
86
|
+
}
|
|
87
|
+
return Object.entries(counts)
|
|
88
|
+
.filter(([, c]) => c > 0)
|
|
89
|
+
.map(([value, count]) => ({ value, count }))
|
|
90
|
+
.sort((a, b) => b.count - a.count)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Visitor breakdown by country, using an explicit `country` column. */
|
|
94
|
+
export async function getCountryBreakdown(n = 10): Promise<Array<{ value: string, count: number }>> {
|
|
95
|
+
const Request = await loadModel('Request')
|
|
96
|
+
const rows = await safeAll(Request)
|
|
97
|
+
return topN(rows, 'country', n)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Top page paths by request count. */
|
|
101
|
+
export async function getTopPages(n = 10): Promise<Array<{ value: string, count: number }>> {
|
|
102
|
+
const Request = await loadModel('Request')
|
|
103
|
+
const rows = await safeAll(Request)
|
|
104
|
+
return topN(rows, 'path', n)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SalesTimeSeries {
|
|
108
|
+
perDay: Array<{ date: string, total: number }>
|
|
109
|
+
totalRevenue: number
|
|
110
|
+
orderCount: number
|
|
111
|
+
avgOrderValue: number
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Revenue-per-day plus rollups, derived from the `Order` model.
|
|
116
|
+
* Falls back to zero values if the model isn't loaded.
|
|
117
|
+
*/
|
|
118
|
+
export async function getSalesTimeSeries(days = 30): Promise<SalesTimeSeries> {
|
|
119
|
+
const Order = await loadModel('Order')
|
|
120
|
+
const rows = await safeAll(Order)
|
|
121
|
+
let totalRevenue = 0
|
|
122
|
+
for (const r of rows) {
|
|
123
|
+
totalRevenue += Number(safeGet(r, 'total_amount', 0)) || Number(safeGet(r, 'amount', 0)) || 0
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
perDay: sumByDay(rows, 'created_at', 'total_amount', days),
|
|
127
|
+
totalRevenue,
|
|
128
|
+
orderCount: rows.length,
|
|
129
|
+
avgOrderValue: rows.length > 0 ? totalRevenue / rows.length : 0,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Top-selling products by occurrence in OrderItems. */
|
|
134
|
+
export async function getTopProducts(n = 5): Promise<Array<{ value: string, count: number }>> {
|
|
135
|
+
const OrderItem = await loadModel('OrderItem')
|
|
136
|
+
const rows = await safeAll(OrderItem)
|
|
137
|
+
return topN(rows, 'product_name', n)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Top customers by order count. */
|
|
141
|
+
export async function getTopCustomers(n = 5): Promise<Array<{ value: string, count: number }>> {
|
|
142
|
+
const Order = await loadModel('Order')
|
|
143
|
+
const rows = await safeAll(Order)
|
|
144
|
+
return topN(rows, 'customer_name', n)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface MarketingMetrics {
|
|
148
|
+
campaignCount: number
|
|
149
|
+
listCount: number
|
|
150
|
+
subscriberCount: number
|
|
151
|
+
socialPostCount: number
|
|
152
|
+
reviewCount: number
|
|
153
|
+
campaignsByStatus: Record<string, number>
|
|
154
|
+
subscribersPerDay: Array<{ date: string, count: number }>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function getMarketingMetrics(days = 30): Promise<MarketingMetrics> {
|
|
158
|
+
const [Campaign, EmailList, Subscriber, SocialPost, Review] = await Promise.all([
|
|
159
|
+
loadModel('Campaign'),
|
|
160
|
+
loadModel('EmailList'),
|
|
161
|
+
loadModel('Subscriber'),
|
|
162
|
+
loadModel('SocialPost'),
|
|
163
|
+
loadModel('Review'),
|
|
164
|
+
])
|
|
165
|
+
const [campaigns, lists, subscribers, posts, reviews] = await Promise.all([
|
|
166
|
+
safeAll(Campaign),
|
|
167
|
+
safeAll(EmailList),
|
|
168
|
+
safeAll(Subscriber),
|
|
169
|
+
safeAll(SocialPost),
|
|
170
|
+
safeAll(Review),
|
|
171
|
+
])
|
|
172
|
+
return {
|
|
173
|
+
campaignCount: campaigns.length,
|
|
174
|
+
listCount: lists.length,
|
|
175
|
+
subscriberCount: subscribers.length,
|
|
176
|
+
socialPostCount: posts.length,
|
|
177
|
+
reviewCount: reviews.length,
|
|
178
|
+
campaignsByStatus: countBy(campaigns, 'status'),
|
|
179
|
+
subscribersPerDay: groupByDay(subscribers, 'created_at', days),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Goal/event tracking from the Activity model. */
|
|
184
|
+
export async function getEventStats(): Promise<{
|
|
185
|
+
total: number
|
|
186
|
+
byEvent: Record<string, number>
|
|
187
|
+
perDay: Array<{ date: string, count: number }>
|
|
188
|
+
}> {
|
|
189
|
+
const Activity = await loadModel('Activity')
|
|
190
|
+
const rows = await safeAll(Activity)
|
|
191
|
+
return {
|
|
192
|
+
total: rows.length,
|
|
193
|
+
byEvent: countBy(rows, 'event'),
|
|
194
|
+
perDay: groupByDay(rows, 'created_at', 30),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Blog-content metrics — uses Post + Comment + Tag models. */
|
|
199
|
+
export async function getBlogMetrics(): Promise<{
|
|
200
|
+
postCount: number
|
|
201
|
+
publishedCount: number
|
|
202
|
+
draftCount: number
|
|
203
|
+
totalViews: number
|
|
204
|
+
totalComments: number
|
|
205
|
+
postsPerDay: Array<{ date: string, count: number }>
|
|
206
|
+
}> {
|
|
207
|
+
const [Post, Comment] = await Promise.all([loadModel('Post'), loadModel('Comment')])
|
|
208
|
+
const [posts, comments] = await Promise.all([safeAll(Post), safeAll(Comment)])
|
|
209
|
+
let totalViews = 0
|
|
210
|
+
let publishedCount = 0
|
|
211
|
+
let draftCount = 0
|
|
212
|
+
for (const p of posts) {
|
|
213
|
+
totalViews += Number(safeGet(p, 'views', 0)) || 0
|
|
214
|
+
const status = String(safeGet(p, 'status', '')).toLowerCase()
|
|
215
|
+
if (status === 'published') publishedCount++
|
|
216
|
+
else if (status === 'draft') draftCount++
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
postCount: posts.length,
|
|
220
|
+
publishedCount,
|
|
221
|
+
draftCount,
|
|
222
|
+
totalViews,
|
|
223
|
+
totalComments: comments.length,
|
|
224
|
+
postsPerDay: groupByDay(posts, 'created_at', 30),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Cheap heuristic to bucket a UA string into a browser family. */
|
|
229
|
+
function parseBrowser(ua: string): string {
|
|
230
|
+
if (!ua) return 'unknown'
|
|
231
|
+
const u = ua.toLowerCase()
|
|
232
|
+
if (u.includes('edg/')) return 'Edge'
|
|
233
|
+
if (u.includes('opr/') || u.includes('opera')) return 'Opera'
|
|
234
|
+
if (u.includes('chrome')) return 'Chrome'
|
|
235
|
+
if (u.includes('safari') && !u.includes('chrome')) return 'Safari'
|
|
236
|
+
if (u.includes('firefox')) return 'Firefox'
|
|
237
|
+
return 'Other'
|
|
238
|
+
}
|