@coursebuilder/analytics 1.1.0 → 1.1.2-canary.0.0c9beb2ae

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.
@@ -3,7 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/components/omnibus-dashboard.tsx
5
5
  import React3, { useState as useState2, useTransition } from "react";
6
- import { CheckIcon, ChevronDownIcon, ClipboardIcon, ClipboardListIcon, ClockIcon, DollarSignIcon, ExternalLinkIcon, FilmIcon, GlobeIcon, LinkIcon, Loader2Icon, MousePointerClickIcon, PlayIcon, ShoppingCartIcon, TrendingUpIcon } from "lucide-react";
6
+ import { CheckIcon, ChevronDownIcon, ClipboardIcon, ClipboardListIcon, ClockIcon, DollarSignIcon, ExternalLinkIcon, FilmIcon, GlobeIcon, LinkIcon, Loader2Icon, MousePointerClickIcon, PlayIcon, ShoppingCartIcon, TrendingUpIcon, RouteIcon } from "lucide-react";
7
7
  import { parseAsStringLiteral, useQueryState } from "nuqs";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@coursebuilder/ui";
9
9
 
@@ -684,6 +684,88 @@ function ShortlinksCard({ shortlinks, totalClicks }) {
684
684
  }), expanded ? "Show less" : `Show ${shortlinks.length - 10} more`)));
685
685
  }
686
686
  __name(ShortlinksCard, "ShortlinksCard");
687
+ function fmtPct(v) {
688
+ return `${v.toFixed(1)}%`;
689
+ }
690
+ __name(fmtPct, "fmtPct");
691
+ function TrafficBreakdownTable({ title, label, rows, getName }) {
692
+ if (!rows.length)
693
+ return null;
694
+ return /* @__PURE__ */ React3.createElement("section", {
695
+ className: "min-w-0"
696
+ }, /* @__PURE__ */ React3.createElement("div", {
697
+ className: "mb-2 flex items-baseline justify-between gap-2"
698
+ }, /* @__PURE__ */ React3.createElement("h3", {
699
+ className: "text-sm font-semibold"
700
+ }, title), /* @__PURE__ */ React3.createElement("span", {
701
+ className: "text-muted-foreground text-[11px]"
702
+ }, "Top ", rows.length)), /* @__PURE__ */ React3.createElement("div", {
703
+ className: "overflow-x-auto rounded-xl border border-border/50"
704
+ }, /* @__PURE__ */ React3.createElement("table", {
705
+ className: "w-full min-w-[440px] text-left text-sm"
706
+ }, /* @__PURE__ */ React3.createElement("thead", {
707
+ className: "bg-muted/30 text-muted-foreground text-[11px] uppercase tracking-wide"
708
+ }, /* @__PURE__ */ React3.createElement("tr", null, /* @__PURE__ */ React3.createElement("th", {
709
+ className: "px-3 py-2 font-medium"
710
+ }, label), /* @__PURE__ */ React3.createElement("th", {
711
+ className: "px-3 py-2 text-right font-medium"
712
+ }, "Sessions"), /* @__PURE__ */ React3.createElement("th", {
713
+ className: "px-3 py-2 text-right font-medium"
714
+ }, "Users"), /* @__PURE__ */ React3.createElement("th", {
715
+ className: "px-3 py-2 text-right font-medium"
716
+ }, "In rows"), /* @__PURE__ */ React3.createElement("th", {
717
+ className: "px-3 py-2 text-right font-medium"
718
+ }, "Of traffic"))), /* @__PURE__ */ React3.createElement("tbody", null, rows.map((row) => /* @__PURE__ */ React3.createElement("tr", {
719
+ key: getName(row),
720
+ className: "border-t border-border/40 tabular-nums"
721
+ }, /* @__PURE__ */ React3.createElement("td", {
722
+ className: "max-w-[220px] truncate px-3 py-2 font-medium"
723
+ }, getName(row)), /* @__PURE__ */ React3.createElement("td", {
724
+ className: "px-3 py-2 text-right"
725
+ }, row.sessions.toLocaleString()), /* @__PURE__ */ React3.createElement("td", {
726
+ className: "px-3 py-2 text-right"
727
+ }, row.users.toLocaleString()), /* @__PURE__ */ React3.createElement("td", {
728
+ className: "px-3 py-2 text-right"
729
+ }, fmtPct(row.sessionPercent)), /* @__PURE__ */ React3.createElement("td", {
730
+ className: "px-3 py-2 text-right"
731
+ }, fmtPct(row.trafficSessionPercent))))))));
732
+ }
733
+ __name(TrafficBreakdownTable, "TrafficBreakdownTable");
734
+ function TrafficBreakdownsCard({ traffic }) {
735
+ const deviceCategories = traffic.deviceCategories ?? [];
736
+ const operatingSystems = traffic.operatingSystems ?? [];
737
+ const screenResolutions = traffic.screenResolutions ?? [];
738
+ if (deviceCategories.length === 0 && operatingSystems.length === 0 && screenResolutions.length === 0) {
739
+ return null;
740
+ }
741
+ return /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, null, /* @__PURE__ */ React3.createElement("div", {
742
+ className: "flex items-center gap-2.5"
743
+ }, /* @__PURE__ */ React3.createElement(GlobeIcon, {
744
+ className: "text-muted-foreground h-4 w-4"
745
+ }), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
746
+ className: "text-sm font-semibold"
747
+ }, "Traffic Details"), /* @__PURE__ */ React3.createElement(CardDescription, {
748
+ className: "text-[11px]"
749
+ }, "Device, OS, and screen share. In rows is share inside the table. Of traffic is share of total sessions.")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
750
+ className: "grid gap-4 xl:grid-cols-3"
751
+ }, /* @__PURE__ */ React3.createElement(TrafficBreakdownTable, {
752
+ title: "Device category",
753
+ label: "Device",
754
+ rows: deviceCategories,
755
+ getName: (row) => row.deviceCategory
756
+ }), /* @__PURE__ */ React3.createElement(TrafficBreakdownTable, {
757
+ title: "Operating systems",
758
+ label: "OS",
759
+ rows: operatingSystems.slice(0, 10),
760
+ getName: (row) => row.operatingSystem
761
+ }), /* @__PURE__ */ React3.createElement(TrafficBreakdownTable, {
762
+ title: "Screen resolutions",
763
+ label: "Resolution",
764
+ rows: screenResolutions.slice(0, 10),
765
+ getName: (row) => row.screenResolution
766
+ }))));
767
+ }
768
+ __name(TrafficBreakdownsCard, "TrafficBreakdownsCard");
687
769
  var RANGES = [
688
770
  {
689
771
  value: "24h",
@@ -731,6 +813,7 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
731
813
  const hasTraffic = data.traffic != null;
732
814
  const hasSurveys = data.surveySegments != null && data.surveySegments.length > 0;
733
815
  const hasSurveyCorrelation = data.surveyCorrelation != null;
816
+ const hasValuePaths = data.valuePaths != null && data.valuePaths.contacts > 0;
734
817
  const hasShortlinks = data.shortlinks.length > 0;
735
818
  return /* @__PURE__ */ React3.createElement("div", {
736
819
  className: "flex flex-col gap-5 lg:gap-7"
@@ -742,7 +825,7 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
742
825
  className: "text-pretty text-xl font-bold tracking-tight sm:text-2xl"
743
826
  }, "Analytics"), /* @__PURE__ */ React3.createElement("p", {
744
827
  className: "text-muted-foreground truncate text-[13px]"
745
- }, "Revenue \xB7 Attribution \xB7 Video \xB7 Traffic \xB7 Surveys \u2014 ", rangeLabel)), /* @__PURE__ */ React3.createElement("div", {
828
+ }, "Revenue \xB7 Attribution \xB7 Video \xB7 Traffic \xB7 Surveys, ", rangeLabel)), /* @__PURE__ */ React3.createElement("div", {
746
829
  className: "flex shrink-0 items-center gap-2"
747
830
  }, isPending && /* @__PURE__ */ React3.createElement("span", {
748
831
  className: "text-muted-foreground animate-pulse text-xs"
@@ -787,12 +870,19 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
787
870
  value: fmtK(totalClicks),
788
871
  sub: `${data.shortlinks.length} active \xB7 ${signupCount} signups`,
789
872
  icon: MousePointerClickIcon
873
+ }), hasValuePaths && /* @__PURE__ */ React3.createElement(Stat, {
874
+ label: "Value Paths",
875
+ value: `${data.valuePaths.terminalParticipants}/${data.valuePaths.contacts}`,
876
+ sub: `${data.valuePaths.answerEvents} answers \xB7 ${data.valuePaths.dripEvents} drips`,
877
+ icon: RouteIcon
790
878
  }), hasSurveys && /* @__PURE__ */ React3.createElement(Stat, {
791
879
  label: "Surveys",
792
880
  value: `${data.surveySegments.length}`,
793
881
  sub: `${data.surveySegments.reduce((s, q) => s + q.responses, 0).toLocaleString()} responses`,
794
882
  icon: ClipboardListIcon
795
- })), hasMux && data.mux.topVideos.length > 0 && /* @__PURE__ */ React3.createElement(TopVideosCard, {
883
+ })), hasTraffic && /* @__PURE__ */ React3.createElement(TrafficBreakdownsCard, {
884
+ traffic: data.traffic
885
+ }), hasMux && data.mux.topVideos.length > 0 && /* @__PURE__ */ React3.createElement(TopVideosCard, {
796
886
  label: "Site Videos",
797
887
  subtitle: `${fmtWatchMs(data.mux.overview.totalPlayingTimeMs)} watch time \xB7 ${fmtK(data.mux.overview.uniqueViewers)} viewers`,
798
888
  icon: FilmIcon,
@@ -834,7 +924,7 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
834
924
  className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
835
925
  }, "Conversion Rate"), /* @__PURE__ */ React3.createElement("span", {
836
926
  className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
837
- }, signupCount > 0 ? `${(purchaseAttrCount / signupCount * 100).toFixed(1)}%` : "\u2014"))))), hasAttributionCoverage && data.attributionCoverage.totalPurchases > 0 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
927
+ }, signupCount > 0 ? `${(purchaseAttrCount / signupCount * 100).toFixed(1)}%` : "-"))))), hasAttributionCoverage && data.attributionCoverage.totalPurchases > 0 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
838
928
  className: "pb-3"
839
929
  }, /* @__PURE__ */ React3.createElement("div", {
840
930
  className: "flex items-center gap-2.5"
@@ -866,7 +956,109 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
866
956
  className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
867
957
  }, "Total Purchases"), /* @__PURE__ */ React3.createElement("span", {
868
958
  className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
869
- }, data.attributionCoverage.totalPurchases.toLocaleString()))))), hasSurveys && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
959
+ }, data.attributionCoverage.totalPurchases.toLocaleString()))), data.attributionCoverage.quality && /* @__PURE__ */ React3.createElement("div", {
960
+ className: "mt-4 grid gap-2 sm:grid-cols-4"
961
+ }, [
962
+ "strong",
963
+ "medium",
964
+ "weak",
965
+ "unknown"
966
+ ].map((lane) => {
967
+ const item = data.attributionCoverage.quality[lane];
968
+ return /* @__PURE__ */ React3.createElement("div", {
969
+ key: lane,
970
+ className: "rounded-lg border border-border/40 p-3"
971
+ }, /* @__PURE__ */ React3.createElement("span", {
972
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
973
+ }, lane), /* @__PURE__ */ React3.createElement("span", {
974
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
975
+ }, fmt$(item.revenue)), /* @__PURE__ */ React3.createElement("span", {
976
+ className: "text-muted-foreground text-[11px]"
977
+ }, item.purchases.toLocaleString(), " purchases \xB7", " ", (item.revenuePercent * 100).toFixed(1), "%"));
978
+ })))), hasValuePaths && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
979
+ className: "pb-3"
980
+ }, /* @__PURE__ */ React3.createElement("div", {
981
+ className: "flex items-center gap-2.5"
982
+ }, /* @__PURE__ */ React3.createElement("div", {
983
+ className: "flex h-7 w-7 items-center justify-center rounded-lg bg-sky-500/10 text-sky-500"
984
+ }, /* @__PURE__ */ React3.createElement(RouteIcon, {
985
+ className: "h-3.5 w-3.5"
986
+ })), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
987
+ className: "text-sm font-semibold"
988
+ }, "Value Paths"), /* @__PURE__ */ React3.createElement(CardDescription, {
989
+ className: "text-[11px]"
990
+ }, "Progression, answer clicks, drip fallback, and terminal completion")))), /* @__PURE__ */ React3.createElement(CardContent, {
991
+ className: "space-y-5"
992
+ }, (() => {
993
+ const vp = data.valuePaths;
994
+ const completionRate = vp.contacts > 0 ? vp.terminalParticipants / vp.contacts * 100 : 0;
995
+ const clickRate = vp.contacts > 0 ? vp.participantsWithAnswerClicks / vp.contacts * 100 : 0;
996
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", {
997
+ className: "grid grid-cols-2 gap-2 sm:grid-cols-5"
998
+ }, /* @__PURE__ */ React3.createElement("div", {
999
+ className: "bg-muted/20 rounded-lg p-3"
1000
+ }, /* @__PURE__ */ React3.createElement("span", {
1001
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
1002
+ }, "Contacts"), /* @__PURE__ */ React3.createElement("span", {
1003
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
1004
+ }, vp.contacts.toLocaleString())), /* @__PURE__ */ React3.createElement("div", {
1005
+ className: "bg-muted/20 rounded-lg p-3"
1006
+ }, /* @__PURE__ */ React3.createElement("span", {
1007
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
1008
+ }, "Terminal"), /* @__PURE__ */ React3.createElement("span", {
1009
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
1010
+ }, vp.terminalParticipants.toLocaleString()), /* @__PURE__ */ React3.createElement("span", {
1011
+ className: "text-muted-foreground/70 text-[10px]"
1012
+ }, completionRate.toFixed(0), "% completed")), /* @__PURE__ */ React3.createElement("div", {
1013
+ className: "bg-muted/20 rounded-lg p-3"
1014
+ }, /* @__PURE__ */ React3.createElement("span", {
1015
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
1016
+ }, "Answered"), /* @__PURE__ */ React3.createElement("span", {
1017
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
1018
+ }, vp.participantsWithAnswerClicks.toLocaleString()), /* @__PURE__ */ React3.createElement("span", {
1019
+ className: "text-muted-foreground/70 text-[10px]"
1020
+ }, clickRate.toFixed(0), "% clicked")), /* @__PURE__ */ React3.createElement("div", {
1021
+ className: "bg-muted/20 rounded-lg p-3"
1022
+ }, /* @__PURE__ */ React3.createElement("span", {
1023
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
1024
+ }, "Drips"), /* @__PURE__ */ React3.createElement("span", {
1025
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
1026
+ }, vp.dripEvents.toLocaleString())), /* @__PURE__ */ React3.createElement("div", {
1027
+ className: "bg-muted/20 rounded-lg p-3"
1028
+ }, /* @__PURE__ */ React3.createElement("span", {
1029
+ className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
1030
+ }, "Blocked"), /* @__PURE__ */ React3.createElement("span", {
1031
+ className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
1032
+ }, vp.blockedIntents.toLocaleString()))), vp.answerOptions.length > 0 && /* @__PURE__ */ React3.createElement("div", {
1033
+ className: "space-y-2"
1034
+ }, /* @__PURE__ */ React3.createElement("p", {
1035
+ className: "text-muted-foreground text-[11px]"
1036
+ }, "Top answer choices by captured answer event"), /* @__PURE__ */ React3.createElement("div", {
1037
+ className: "space-y-1.5"
1038
+ }, vp.answerOptions.slice(0, 8).map((answer) => {
1039
+ const pct = vp.answerEvents > 0 ? answer.count / vp.answerEvents * 100 : 0;
1040
+ return /* @__PURE__ */ React3.createElement("div", {
1041
+ key: answer.key,
1042
+ className: "flex items-center gap-2.5"
1043
+ }, /* @__PURE__ */ React3.createElement("span", {
1044
+ className: "text-muted-foreground w-[140px] shrink-0 truncate text-right text-[12px]",
1045
+ title: answer.key
1046
+ }, answer.optionValue), /* @__PURE__ */ React3.createElement("div", {
1047
+ className: "bg-muted/50 relative h-5 flex-1 overflow-hidden rounded"
1048
+ }, /* @__PURE__ */ React3.createElement("div", {
1049
+ className: "absolute inset-y-0 left-0 rounded bg-sky-500/20",
1050
+ style: {
1051
+ width: `${Math.max(pct, 1)}%`
1052
+ }
1053
+ }), /* @__PURE__ */ React3.createElement("div", {
1054
+ className: "relative flex h-full items-center px-2"
1055
+ }, /* @__PURE__ */ React3.createElement("span", {
1056
+ className: "text-foreground/70 text-[11px] font-medium tabular-nums"
1057
+ }, pct.toFixed(0), "%"))), /* @__PURE__ */ React3.createElement("span", {
1058
+ className: "text-muted-foreground w-10 shrink-0 text-right text-[11px] tabular-nums"
1059
+ }, answer.count.toLocaleString()));
1060
+ }))));
1061
+ })())), hasSurveys && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
870
1062
  className: "pb-3"
871
1063
  }, /* @__PURE__ */ React3.createElement("div", {
872
1064
  className: "flex items-center justify-between"
@@ -1238,13 +1430,13 @@ function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref })
1238
1430
  className: `py-2.5 pr-4 text-right text-sm font-semibold tabular-nums ${p.totalAmount > 0 ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"}`
1239
1431
  }, p.totalAmount > 0 ? fmt$(p.totalAmount) : "Free"), /* @__PURE__ */ React3.createElement("td", {
1240
1432
  className: "py-2.5 pr-4 text-right text-sm tabular-nums"
1241
- }, p.seats ?? "\u2014"), /* @__PURE__ */ React3.createElement("td", {
1433
+ }, p.seats ?? "-"), /* @__PURE__ */ React3.createElement("td", {
1242
1434
  className: "max-w-[200px] truncate py-2.5 pr-4 text-sm font-medium"
1243
1435
  }, p.productName), /* @__PURE__ */ React3.createElement("td", {
1244
1436
  className: "text-muted-foreground hidden max-w-[150px] truncate py-2.5 pr-4 text-sm sm:table-cell"
1245
- }, p.userName ?? p.userEmail ?? "\u2014"), /* @__PURE__ */ React3.createElement("td", {
1437
+ }, p.userName ?? p.userEmail ?? "-"), /* @__PURE__ */ React3.createElement("td", {
1246
1438
  className: "text-muted-foreground hidden py-2.5 pr-4 text-sm sm:table-cell"
1247
- }, p.country ? `${countryFlag2(p.country)} ${p.country}` : "\u2014"), /* @__PURE__ */ React3.createElement("td", {
1439
+ }, p.country ? `${countryFlag2(p.country)} ${p.country}` : "-"), /* @__PURE__ */ React3.createElement("td", {
1248
1440
  className: "text-muted-foreground whitespace-nowrap py-2.5 pr-4 text-sm"
1249
1441
  }, fmtAgo(p.createdAt)))))))))));
1250
1442
  }