lytx 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,126 @@
1
+ import { useContext } from "react";
2
+ import { AuthContext } from "@/app/providers/AuthProvider";
3
+
4
+ const LAST_SITE_KEY = "lytx_last_site_id";
5
+
6
+ /** Save to localStorage for immediate access on next page load */
7
+ function saveLastSiteToStorage(site_id: number): void {
8
+ try {
9
+ localStorage.setItem(LAST_SITE_KEY, String(site_id));
10
+ } catch {
11
+ // localStorage might not be available
12
+ }
13
+ }
14
+
15
+ /** Get last site from localStorage */
16
+ export function getLastSiteFromStorage(): number | null {
17
+ try {
18
+ const stored = localStorage.getItem(LAST_SITE_KEY);
19
+ return stored ? parseInt(stored, 10) : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /** Persist to database for cross-device sync */
26
+ async function updateLastSiteInDB(site_id: number): Promise<void> {
27
+ try {
28
+ const response = await fetch("/api/user/update-last-site", {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({ site_id }),
32
+ });
33
+ if (!response.ok) {
34
+ console.error("Failed to update last site:", await response.text());
35
+ }
36
+ } catch (error) {
37
+ console.error("Error updating last site:", error);
38
+ }
39
+ }
40
+
41
+ type SiteSelectorProps = {
42
+ callBack?: (opts: { name: string; id: number; tag_id: string }) => void;
43
+ initialSites?: Array<{ site_id: number; name: string; tag_id: string }>;
44
+ initialSiteId?: number | null;
45
+ wrapperClassName?: string;
46
+ selectClassName?: string;
47
+ };
48
+ export const SiteSelector: React.FC<SiteSelectorProps> = ({
49
+ initialSites = [],
50
+ initialSiteId = null,
51
+ wrapperClassName,
52
+ selectClassName,
53
+ }) => {
54
+ const {
55
+ data: session,
56
+ current_site,
57
+ setCurrentSite,
58
+ } = useContext(AuthContext);
59
+
60
+ const sites = session?.userSites && session.userSites.length > 0
61
+ ? session.userSites.map((site) => ({
62
+ site_id: site.site_id,
63
+ name: site.name || `Site ${site.site_id}`,
64
+ tag_id: site.tag_id,
65
+ }))
66
+ : initialSites;
67
+
68
+ const selectedSiteName = current_site?.name
69
+ ?? sites.find((site) => site.site_id === initialSiteId)?.name
70
+ ?? sites[0]?.name
71
+ ?? "";
72
+
73
+ const combinedWrapperClassName = ["relative inline-block", wrapperClassName]
74
+ .filter(Boolean)
75
+ .join(" ");
76
+
77
+ return (
78
+ <div className={combinedWrapperClassName}>
79
+ <select
80
+ value={selectedSiteName}
81
+ onChange={(e) => {
82
+ const selectedSite = sites.find(
83
+ (site) => site.name === e.target.value,
84
+ );
85
+ if (selectedSite) {
86
+ setCurrentSite({
87
+ name: selectedSite.name,
88
+ id: selectedSite.site_id,
89
+ tag_id: selectedSite.tag_id,
90
+ });
91
+ // Save to localStorage for immediate access on page reload
92
+ saveLastSiteToStorage(selectedSite.site_id);
93
+ // Persist to database for cross-device sync (fire and forget)
94
+ updateLastSiteInDB(selectedSite.site_id);
95
+ }
96
+ }}
97
+ className={`appearance-none bg-[var(--theme-input-bg)] pl-4 pr-10 py-2 text-sm text-left text-[var(--theme-text-primary)] rounded-lg border border-[var(--theme-input-border)] focus:border-[var(--theme-border-primary)] focus:outline-none transition-colors ${selectClassName ?? ""}`}
98
+ >
99
+ {sites.length > 0
100
+ ? sites.map((site) => (
101
+ <option key={site.site_id} value={site.name} className="text-left">
102
+ {site.name}
103
+ </option>
104
+ ))
105
+ : ""}
106
+ </select>
107
+ <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[var(--theme-text-secondary)]">
108
+ <svg
109
+ aria-hidden="true"
110
+ className="h-4 w-4"
111
+ viewBox="0 0 20 20"
112
+ fill="none"
113
+ xmlns="http://www.w3.org/2000/svg"
114
+ >
115
+ <path
116
+ d="M5 7.5L10 12.5L15 7.5"
117
+ stroke="currentColor"
118
+ strokeWidth="1.8"
119
+ strokeLinecap="round"
120
+ strokeLinejoin="round"
121
+ />
122
+ </svg>
123
+ </span>
124
+ </div>
125
+ );
126
+ };
@@ -0,0 +1,42 @@
1
+ import { LYTX_SCRIPT_PATH } from "@/app/constants";
2
+
3
+ export type SiteTagProps = {
4
+ tag_id: string;
5
+ domain: string;
6
+ lytx_domain?: string;
7
+ };
8
+
9
+ export const SiteTag: React.FC<SiteTagProps> = ({
10
+ tag_id,
11
+ domain,
12
+ lytx_domain,
13
+ }) => {
14
+ // Infer the Lytx domain from the current host if not provided
15
+ if (!lytx_domain) {
16
+ lytx_domain = typeof window !== "undefined"
17
+ ? window.location.host
18
+ : "localhost:5173";
19
+ }
20
+
21
+ return (
22
+ <code className="text-sm">
23
+ <span style={{ color: "#6c7086" }}>&lt;</span>
24
+ <span style={{ color: "#f38ba8", fontWeight: "600" }}>script</span>
25
+ <span style={{ color: "#6c7086" }}> </span>
26
+ <span style={{ color: "#a6e3a1" }}>defer</span>
27
+ <span style={{ color: "#6c7086" }}> </span>
28
+ <span style={{ color: "#a6e3a1" }}>data-domain</span>
29
+ <span style={{ color: "#6c7086" }}>=</span>
30
+ <span style={{ color: "#fab387" }}>"{domain}"</span>
31
+ <span style={{ color: "#6c7086" }}> </span>
32
+ <span style={{ color: "#a6e3a1" }}>src</span>
33
+ <span style={{ color: "#6c7086" }}>=</span>
34
+ <span style={{ color: "#fab387" }}>
35
+ "https://{lytx_domain}{LYTX_SCRIPT_PATH}?account={tag_id}"
36
+ </span>
37
+ <span style={{ color: "#6c7086" }}>&gt;&lt;/</span>
38
+ <span style={{ color: "#f38ba8", fontWeight: "600" }}>script</span>
39
+ <span style={{ color: "#6c7086" }}>&gt;</span>
40
+ </code>
41
+ );
42
+ };
@@ -0,0 +1,241 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Highlight, themes } from "prism-react-renderer";
5
+ import { Button } from "@/app/components/ui/Button";
6
+ import { LYTX_SCRIPT_PATH } from "@/app/constants";
7
+
8
+ type SiteTagInstallCardProps = {
9
+ site: {
10
+ site_id?: number;
11
+ name?: string | null;
12
+ domain?: string | null;
13
+ tag_id: string;
14
+ createdAt?: string | Date | null;
15
+ };
16
+ className?: string;
17
+ };
18
+
19
+ function CodeBlock({
20
+ code,
21
+ language,
22
+ id,
23
+ }: {
24
+ code: string;
25
+ language: string;
26
+ id: string;
27
+ }) {
28
+ return (
29
+ <div
30
+ id={id}
31
+ className="bg-[var(--theme-input-bg)] border border-[var(--theme-input-border)] rounded-md overflow-hidden"
32
+ >
33
+ <Highlight theme={themes.vsDark} code={code} language={language}>
34
+ {({ className: highlightClassName, style, tokens, getLineProps, getTokenProps }) => (
35
+ <pre
36
+ className={`${highlightClassName} p-4 text-sm overflow-x-auto`}
37
+ style={style}
38
+ >
39
+ {tokens.map((line, i) => (
40
+ <div key={i} {...getLineProps({ line })}>
41
+ {line.map((token, key) => (
42
+ <span key={key} {...getTokenProps({ token })} />
43
+ ))}
44
+ </div>
45
+ ))}
46
+ </pre>
47
+ )}
48
+ </Highlight>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ export function SiteTagInstallCard({ site, className }: SiteTagInstallCardProps) {
54
+ const [activeInstallTab, setActiveInstallTab] = useState("html");
55
+ const [lytxDomain, setLytxDomain] = useState("https://lytx.io");
56
+
57
+ useEffect(() => {
58
+ setLytxDomain(window.location.origin);
59
+ }, []);
60
+ const containerClassName = className
61
+ ? `border border-[var(--theme-border-primary)] rounded-lg bg-[var(--theme-bg-secondary)] p-4 sm:p-6 ${className}`
62
+ : "border border-[var(--theme-border-primary)] rounded-lg bg-[var(--theme-bg-secondary)] p-4 sm:p-6";
63
+
64
+ const getCodeForTab = (tab: string) => {
65
+ switch (tab) {
66
+ case "html":
67
+ return `<script defer \n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n></script>`;
68
+ case "rwsdk":
69
+ return `// In your Document.tsx\nexport const Document = ({ children }) => (\n <html>\n <head>{/* ... */}</head>\n <body>\n <div id="root">{children}</div>\n <script>import("/src/client.tsx")</script>\n <script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n ></script>\n </body>\n </html>\n);`;
70
+ case "sveltekit":
71
+ return `<!-- In your src/app.html -->\n<script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n></script>\n\n<!-- Or in your +layout.svelte -->\n<svelte:head>\n <script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n ></script>\n</svelte:head>`;
72
+ case "solid":
73
+ return `// In your root.tsx or index.html\nimport { Script } from "@solidjs/meta";\n\nexport default function Root() {\n return (\n <Html>\n <Head>\n <Script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n />\n </Head>\n <Body>\n {/* Your app content */}\n </Body>\n </Html>\n );\n}`;
74
+ case "nextjs":
75
+ return `// In your _document.tsx or layout.tsx (App Router)\nimport Script from 'next/script';\n\nexport default function RootLayout({ children }) {\n return (\n <html>\n <head>\n <Script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n strategy="beforeInteractive"\n />\n </head>\n <body>{children}</body>\n </html>\n );\n}`;
76
+ case "nuxt":
77
+ return `// In your nuxt.config.ts\nexport default defineNuxtConfig({\n app: {\n head: {\n script: [\n {\n src: '${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}',\n defer: true\n }\n ]\n }\n }\n});\n\n// Or use useHead() in a component/page\nuseHead({\n script: [\n {\n src: '${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}',\n defer: true\n }\n ]\n});`;
78
+ default:
79
+ return "";
80
+ }
81
+ };
82
+
83
+ return (
84
+ <div className={containerClassName}>
85
+ <div className="mb-4 flex items-start justify-between">
86
+ <div className="flex-1">
87
+ <div className="mb-2 flex flex-wrap items-center gap-2 sm:gap-3">
88
+ <h3 className="text-lg font-medium text-[var(--theme-text-primary)]">
89
+ {site.name || "Selected Site"}
90
+ </h3>
91
+ <span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
92
+ active
93
+ </span>
94
+ </div>
95
+ <div className="text-sm text-[var(--theme-text-secondary)] space-y-1">
96
+ <p>
97
+ <span className="font-medium">Domain:</span>{" "}
98
+ {site.domain || "—"}
99
+ </p>
100
+ <p>
101
+ <span className="font-medium">Tag ID:</span>{" "}
102
+ <code className="break-all rounded bg-[var(--theme-input-bg)] px-2 py-1 text-xs">
103
+ {site.tag_id}
104
+ </code>
105
+ </p>
106
+ <p>
107
+ <span className="font-medium">Created:</span>{" "}
108
+ {site.createdAt ? new Date(site.createdAt).toLocaleDateString() : "N/A"}
109
+ </p>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <div className="border-t border-[var(--theme-border-primary)] pt-4">
115
+ <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
116
+ <label className="text-sm font-medium text-[var(--theme-text-primary)]">
117
+ Installation Instructions
118
+ </label>
119
+ <Button
120
+ variant="primary"
121
+ size="sm"
122
+ className="w-full sm:w-auto"
123
+ onClick={() => {
124
+ const code = getCodeForTab(activeInstallTab);
125
+ if (code) {
126
+ navigator.clipboard.writeText(code);
127
+ alert("Code copied to clipboard!");
128
+ }
129
+ }}
130
+ >
131
+ Copy Code
132
+ </Button>
133
+ </div>
134
+
135
+ <div className="mb-4 overflow-x-auto rounded-lg bg-[var(--theme-bg-secondary)] p-1">
136
+ <div className="flex min-w-max space-x-1">
137
+ {[
138
+ { id: "html", label: "HTML" },
139
+ { id: "rwsdk", label: "RWSDK" },
140
+ { id: "sveltekit", label: "SvelteKit" },
141
+ { id: "solid", label: "Solid" },
142
+ { id: "nextjs", label: "Next.js" },
143
+ { id: "nuxt", label: "Nuxt" },
144
+ ].map((tab) => (
145
+ <button
146
+ key={tab.id}
147
+ onClick={() => setActiveInstallTab(tab.id)}
148
+ className={`whitespace-nowrap rounded-md px-3 py-2 text-xs font-medium transition-colors ${activeInstallTab === tab.id
149
+ ? "bg-[var(--theme-bg-primary)] text-[var(--theme-text-primary)] shadow-sm"
150
+ : "text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)]"
151
+ }`}
152
+ >
153
+ {tab.label}
154
+ </button>
155
+ ))}
156
+ </div>
157
+ </div>
158
+
159
+ <div className="space-y-4">
160
+ {activeInstallTab === "html" && (
161
+ <div>
162
+ <CodeBlock
163
+ id="install-content-html"
164
+ language="html"
165
+ code={`<script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n></script>`}
166
+ />
167
+ <p className="text-xs text-[var(--theme-text-secondary)] mt-2">
168
+ Copy and paste this script into the &lt;head&gt; section of your HTML.
169
+ </p>
170
+ </div>
171
+ )}
172
+
173
+ {activeInstallTab === "rwsdk" && (
174
+ <div>
175
+ <CodeBlock
176
+ id="install-content-rwsdk"
177
+ language="tsx"
178
+ code={`// In your Document.tsx\nexport const Document = ({ children }) => (\n <html>\n <head>{/* ... */}</head>\n <body>\n <div id="root">{children}</div>\n <script>import("/src/client.tsx")</script>\n <script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n ></script>\n </body>\n </html>\n);`}
179
+ />
180
+ <p className="text-xs text-[var(--theme-text-secondary)] mt-2">
181
+ Add this to your Document.tsx or root layout component.
182
+ </p>
183
+ </div>
184
+ )}
185
+
186
+ {activeInstallTab === "sveltekit" && (
187
+ <div>
188
+ <CodeBlock
189
+ id="install-content-sveltekit"
190
+ language="html"
191
+ code={`<!-- In your src/app.html -->\n<script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n></script>\n\n<!-- Or in your +layout.svelte -->\n<svelte:head>\n <script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n ></script>\n</svelte:head>`}
192
+ />
193
+ <p className="text-xs text-[var(--theme-text-secondary)] mt-2">
194
+ Add the script to your SvelteKit app.html or layout.
195
+ </p>
196
+ </div>
197
+ )}
198
+
199
+ {activeInstallTab === "solid" && (
200
+ <div>
201
+ <CodeBlock
202
+ id="install-content-solid"
203
+ language="tsx"
204
+ code={`// In your root.tsx or index.html\nimport { Script } from "@solidjs/meta";\n\nexport default function Root() {\n return (\n <Html>\n <Head>\n <Script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n />\n </Head>\n <Body>\n {/* Your app content */}\n </Body>\n </Html>\n );\n}`}
205
+ />
206
+ <p className="text-xs text-[var(--theme-text-secondary)] mt-2">
207
+ Place the script tag in your Solid root layout.
208
+ </p>
209
+ </div>
210
+ )}
211
+
212
+ {activeInstallTab === "nextjs" && (
213
+ <div>
214
+ <CodeBlock
215
+ id="install-content-nextjs"
216
+ language="tsx"
217
+ code={`// In your _document.tsx or layout.tsx (App Router)\nimport Script from 'next/script';\n\nexport default function RootLayout({ children }) {\n return (\n <html>\n <head>\n <Script\n defer\n src="${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}"\n strategy="beforeInteractive"\n />\n </head>\n <body>{children}</body>\n </html>\n );\n}`}
218
+ />
219
+ <p className="text-xs text-[var(--theme-text-secondary)] mt-2">
220
+ Add the script to your Next.js document or root layout.
221
+ </p>
222
+ </div>
223
+ )}
224
+
225
+ {activeInstallTab === "nuxt" && (
226
+ <div>
227
+ <CodeBlock
228
+ id="install-content-nuxt"
229
+ language="ts"
230
+ code={`// In your nuxt.config.ts\nexport default defineNuxtConfig({\n app: {\n head: {\n script: [\n {\n src: '${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}',\n defer: true\n }\n ]\n }\n }\n});\n\n// Or use useHead() in a component/page\nuseHead({\n script: [\n {\n src: '${lytxDomain}${LYTX_SCRIPT_PATH}?account=${site.tag_id}',\n defer: true\n }\n ]\n});`}
231
+ />
232
+ <p className="text-xs text-[var(--theme-text-secondary)] mt-2">
233
+ Configure the script in nuxt.config.ts or useHead.
234
+ </p>
235
+ </div>
236
+ )}
237
+ </div>
238
+ </div>
239
+ </div>
240
+ );
241
+ }