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,118 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ type HeadingTag = "h2" | "h3" | "h4";
6
+
7
+ interface DashboardCardProps {
8
+ id?: string;
9
+ title?: React.ReactNode;
10
+ titleAs?: HeadingTag;
11
+ subtitle?: React.ReactNode;
12
+ actions?: React.ReactNode;
13
+ isUpdating?: boolean;
14
+ updatingLabel?: string;
15
+ empty?: boolean;
16
+ emptyState?: React.ReactNode;
17
+ className?: string;
18
+ children?: React.ReactNode;
19
+ }
20
+
21
+ const SpinnerIcon = ({ className }: { className?: string }) => (
22
+ <svg
23
+ aria-hidden="true"
24
+ focusable="false"
25
+ className={className}
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ fill="none"
28
+ viewBox="0 0 24 24"
29
+ >
30
+ <circle
31
+ cx="12"
32
+ cy="12"
33
+ r="10"
34
+ stroke="currentColor"
35
+ strokeWidth="4"
36
+ className="opacity-25"
37
+ />
38
+ <path
39
+ className="opacity-75"
40
+ fill="currentColor"
41
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
42
+ />
43
+ </svg>
44
+ );
45
+
46
+ const UpdatingOverlay = ({ label = "Updating..." }: { label?: string }) => (
47
+ <div
48
+ role="status"
49
+ aria-live="polite"
50
+ className="pointer-events-none absolute inset-0 flex items-center justify-center bg-(--theme-card-bg) bg-opacity-60 z-10 rounded-lg"
51
+ >
52
+ <div className="flex items-center">
53
+ <SpinnerIcon className="w-5 h-5 text-(--theme-text-secondary) animate-spin" />
54
+ <span className="ml-3 text-(--theme-text-secondary)">{label}</span>
55
+ </div>
56
+ </div>
57
+ );
58
+
59
+ const DefaultEmptyState = () => (
60
+ <>
61
+ <p className="text-(--theme-text-secondary)">
62
+ No results match the current filters.
63
+ </p>
64
+ <p className="text-xs text-(--theme-text-secondary) mt-2">
65
+ Try clearing filters or expanding the date range.
66
+ </p>
67
+ </>
68
+ );
69
+
70
+ const BASE_CLASSES =
71
+ "bg-[var(--theme-card-bg)] border border-[var(--theme-card-border)] rounded-lg p-4 sm:p-6";
72
+
73
+ export function DashboardCard({
74
+ id,
75
+ title,
76
+ titleAs: TitleTag = "h2",
77
+ subtitle,
78
+ actions,
79
+ isUpdating,
80
+ updatingLabel,
81
+ empty,
82
+ emptyState,
83
+ className = "",
84
+ children,
85
+ }: DashboardCardProps) {
86
+ const needsRelative = Boolean(isUpdating);
87
+
88
+ return (
89
+ <div
90
+ id={id}
91
+ className={`${BASE_CLASSES} ${needsRelative ? "relative" : ""} ${className}`}
92
+ >
93
+ {(title || actions) && (
94
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between mb-4">
95
+ <div>
96
+ {title && (
97
+ <TitleTag className="text-lg sm:text-xl font-semibold text-(--theme-text-primary)">
98
+ {title}
99
+ </TitleTag>
100
+ )}
101
+ {subtitle && (
102
+ <p className="text-sm text-(--theme-text-secondary)">
103
+ {subtitle}
104
+ </p>
105
+ )}
106
+ </div>
107
+ {actions && <div className="shrink-0">{actions}</div>}
108
+ </div>
109
+ )}
110
+
111
+ {isUpdating && <UpdatingOverlay label={updatingLabel} />}
112
+
113
+ {empty ? (emptyState ?? <DefaultEmptyState />) : children}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export default DashboardCard;
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useRef } from 'react';
4
+ import type { EventFieldDefinition } from './EventForm';
5
+ import type { PageEvent } from "@/templates/lytxpixel";
6
+
7
+ const debug = true;
8
+
9
+ interface EditableCellProps {
10
+ initialValue: any;
11
+ isEditing: boolean;
12
+ fieldDefinition: EventFieldDefinition;
13
+ eventId: string;
14
+ columnId: keyof PageEvent;
15
+ updateData: (eventId: string, columnId: keyof PageEvent, value: any) => void;
16
+ exitEditMode: () => void;
17
+ }
18
+
19
+ const EditableCellComponent: React.FC<EditableCellProps> = ({
20
+ initialValue,
21
+ isEditing,
22
+ fieldDefinition,
23
+ eventId,
24
+ columnId,
25
+ updateData,
26
+ exitEditMode,
27
+ }) => {
28
+
29
+
30
+ // if (debug) {
31
+ // console.log(`RENDER EditableCell: event='${eventId}' col='${String(columnId)}' isEditing=${isEditing} initialValue='${initialValue}'`);
32
+ // }
33
+
34
+ const [currentValue, setCurrentValue] = useState(initialValue);
35
+ const inputRef = useRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(null);
36
+ const prevIsEditingRef = useRef<boolean>(false);
37
+
38
+ useEffect(() => {
39
+ const prevIsEditing = prevIsEditingRef.current;
40
+
41
+ // if (debug) {
42
+ // console.log(`EFFECT EditableCell: event='${eventId}' col='${String(columnId)}'. Prev isEditing: ${prevIsEditing}, Current isEditing: ${isEditing}. InitialValue: '${initialValue}'`);
43
+ // }
44
+
45
+ if (isEditing) {
46
+ // Only set currentValue from initialValue if we are just entering edit mode
47
+ if (prevIsEditing === undefined || prevIsEditing === false) {
48
+ setCurrentValue(initialValue);
49
+ }
50
+ if (inputRef.current) {
51
+ inputRef.current.focus();
52
+ if (inputRef.current instanceof HTMLInputElement || inputRef.current instanceof HTMLTextAreaElement) {
53
+ inputRef.current.select();
54
+ }
55
+ }
56
+ }
57
+ prevIsEditingRef.current = isEditing; // Update for the next render
58
+ }, [isEditing]); // <<<< ENSURE THIS IS THE ONLY DEPENDENCY
59
+
60
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
61
+ setCurrentValue(e.target.value);
62
+ updateData(eventId, columnId, e.target.value);
63
+ };
64
+
65
+ const handleBlur = () => {
66
+ exitEditMode();
67
+ };
68
+
69
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
70
+ if (e.key === 'Enter') {
71
+ if (fieldDefinition.type !== 'select' || (e.target as HTMLSelectElement).type !== 'select-one') {
72
+ e.preventDefault();
73
+ exitEditMode();
74
+ }
75
+ } else if (e.key === 'Escape') {
76
+ setCurrentValue(initialValue);
77
+ updateData(eventId, columnId, initialValue);
78
+ exitEditMode();
79
+ }
80
+ };
81
+
82
+ if (!isEditing) {
83
+ let displayValue: React.ReactNode = initialValue;
84
+ if (fieldDefinition.name === 'Notes' && typeof initialValue === 'string' && initialValue.length > 30) {
85
+ displayValue = `${initialValue.substring(0, 27)}...`;
86
+ } else if (typeof initialValue === 'boolean') {
87
+ displayValue = initialValue ? 'Yes' : 'No';
88
+ } else if (initialValue === null || initialValue === undefined || initialValue === '') {
89
+ displayValue = <span className="text-gray-400 italic">empty</span>;
90
+ }
91
+ return <>{displayValue}</>;
92
+ }
93
+
94
+ const commonProps = {
95
+ ref: inputRef as any,
96
+ value: currentValue ?? '',
97
+ onChange: handleChange,
98
+ onBlur: handleBlur,
99
+ onKeyDown: handleKeyDown,
100
+ className: "w-full p-1 border border-blue-500 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm",
101
+ };
102
+
103
+ if (fieldDefinition.type === 'textarea') {
104
+ return <textarea {...commonProps} rows={2} />;
105
+ }
106
+
107
+ if (fieldDefinition.type === 'select') {
108
+ return (
109
+ <select {...commonProps}>
110
+ {fieldDefinition.options?.map(option => (
111
+ <option key={option} value={option}>
112
+ {String(option)}
113
+ </option>
114
+ ))}
115
+ </select>
116
+ );
117
+ }
118
+
119
+ return <input type="text" {...commonProps} />;
120
+ };
121
+
122
+ export const EditableCell = React.memo(EditableCellComponent);
123
+ // export { EditableCellComponent as EditableCell };
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ // import React from 'react';
4
+ import type { PageEvent } from "@/templates/lytxpixel";
5
+
6
+ export interface EventFieldDefinition {
7
+ name: keyof PageEvent;
8
+ label: string;
9
+ type: 'text' | 'textarea' | 'select';
10
+ options?: string[];
11
+ }
12
+
13
+ interface EventFormProps {
14
+ eventToEdit: PageEvent;
15
+ eventFields: EventFieldDefinition[];
16
+ onEventChange: (fieldName: keyof PageEvent, value: any) => void;
17
+ onCancel: () => void;
18
+ onSubmit: () => void;
19
+ }
20
+
21
+ export function EventForm({
22
+ eventToEdit,
23
+ eventFields,
24
+ onEventChange,
25
+ onCancel,
26
+ onSubmit,
27
+ }: EventFormProps) {
28
+
29
+ if (!eventToEdit) {
30
+ return null;
31
+ }
32
+
33
+ return (
34
+ <div className="p-6 border rounded-lg bg-[var(--theme-card-bg)] border-[var(--theme-card-border)] shadow-md">
35
+ <h3 className="text-xl font-semibold mb-4 text-[var(--theme-text-primary)]">
36
+ Edit Event: {eventToEdit.event_name || 'New Event'}
37
+ </h3>
38
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
39
+ {eventFields.map(field => (
40
+ <div key={field.name}>
41
+ <label htmlFor={`edit-${field.name}`} className="block text-sm font-medium text-[var(--theme-text-secondary)]">
42
+ {field.label}
43
+ </label>
44
+ {field.type === 'textarea' ? (
45
+ <textarea
46
+ id={`edit-${field.name}`}
47
+ value={(eventToEdit as any)[field.name] || ''}
48
+ onChange={(e) => onEventChange(field.name, e.target.value)}
49
+ rows={3}
50
+ className="mt-1 block w-full px-3 py-2 bg-[var(--theme-input-bg)] border border-[var(--theme-input-border)] rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-input-border-focus)] focus:border-[var(--theme-input-border-focus)] sm:text-sm text-[var(--theme-text-primary)]"
51
+ />
52
+ ) : field.type === 'select' ? (
53
+ <select
54
+ id={`edit-${field.name}`}
55
+ value={(eventToEdit as any)[field.name] || ''}
56
+ onChange={(e) => onEventChange(field.name, e.target.value)}
57
+ className="mt-1 block w-full px-3 py-2 bg-[var(--theme-input-bg)] border border-[var(--theme-input-border)] rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-input-border-focus)] focus:border-[var(--theme-input-border-focus)] sm:text-sm text-[var(--theme-text-primary)]"
58
+ >
59
+ {field.options?.map(option => (
60
+ <option key={option} value={option}>{String(option)}</option>
61
+ ))}
62
+ </select>
63
+ ) : ( // type 'text'
64
+ <input
65
+ type={field.type}
66
+ id={`edit-${field.name}`}
67
+ value={(eventToEdit as any)[field.name] || ''}
68
+ onChange={(e) => onEventChange(field.name, e.target.value)}
69
+ className="mt-1 block w-full px-3 py-2 bg-[var(--theme-input-bg)] border border-[var(--theme-input-border)] rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-input-border-focus)] focus:border-[var(--theme-input-border-focus)] sm:text-sm text-[var(--theme-text-primary)]"
70
+ />
71
+ )}
72
+ </div>
73
+ ))}
74
+ </div>
75
+ <div className="mt-6 flex justify-end space-x-3">
76
+ <button
77
+ onClick={onCancel}
78
+ type="button"
79
+ className="px-4 py-2 bg-[var(--theme-bg-secondary)] text-[var(--theme-text-primary)] font-semibold rounded-md shadow-sm hover:bg-[var(--theme-bg-tertiary)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--theme-border-secondary)]"
80
+ >
81
+ Cancel
82
+ </button>
83
+ <button
84
+ onClick={onSubmit}
85
+ type="button"
86
+ className="px-4 py-2 bg-[var(--theme-button-bg)] text-white font-semibold rounded-md shadow-sm hover:bg-[var(--theme-button-hover)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--theme-border-secondary)]"
87
+ >
88
+ Apply Changes (to local)
89
+ </button>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,49 @@
1
+ const Logo = () => (
2
+ <div className="flex items-center gap-2 font-montserrat font-bold text-2xl tracking-tight">
3
+ <img src="/logo.png" alt="Lytx logo" className="h-6 w-6" />
4
+ <span>Lytx</span>
5
+ </div>
6
+ );
7
+
8
+ export function MarketingFooter() {
9
+ return (
10
+ <footer className="bg-white border-t border-slate-200 py-16 dark:bg-black dark:border-slate-800">
11
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
12
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-12">
13
+ <div className="col-span-1 md:col-span-1">
14
+ <Logo />
15
+ <p className="mt-4 text-sm text-slate-500 dark:text-slate-400">
16
+ Privacy-friendly analytics for the modern web. Open source and transparent.
17
+ </p>
18
+ </div>
19
+ <div>
20
+ <h3 className="text-sm font-semibold text-slate-900 uppercase tracking-wider mb-4 dark:text-white">Product</h3>
21
+ <ul className="space-y-3">
22
+ <li><a href="/#features" className="text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">Features</a></li>
23
+ <li><a href="/signup" className="text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">Get Started</a></li>
24
+ <li><a href="/#demo" className="hidden text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">Live Demo</a></li>
25
+ </ul>
26
+ </div>
27
+ <div>
28
+ <h3 className="text-sm font-semibold text-slate-900 uppercase tracking-wider mb-4 dark:text-white">Resources</h3>
29
+ <ul className="space-y-3">
30
+ <li><a href="/api/docs" className="text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">API Documentation</a></li>
31
+ <li><a target="_blank" href="https://github.com/lytx-io/kit" className="text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">GitHub</a></li>
32
+ <li><a href="#" className="hidden text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">Blog</a></li>
33
+ </ul>
34
+ </div>
35
+ <div>
36
+ <h3 className="text-sm font-semibold text-slate-900 uppercase tracking-wider mb-4 dark:text-white">Legal</h3>
37
+ <ul className="space-y-3">
38
+ <li><a href="/privacy" className="text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">Privacy Policy</a></li>
39
+ <li><a href="/terms" className="text-slate-600 hover:text-amber-600 text-sm dark:text-slate-400 dark:hover:text-amber-400">Terms of Service</a></li>
40
+ </ul>
41
+ </div>
42
+ </div>
43
+ <div className="mt-12 pt-8 border-t border-slate-100 dark:border-slate-800 text-center">
44
+ <p className="text-sm text-slate-400">© {new Date().getFullYear()} Lytx. All rights reserved.</p>
45
+ </div>
46
+ </div>
47
+ </footer>
48
+ );
49
+ }
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { ThemeToggle } from "@/app/components/ui/ThemeToggle";
5
+
6
+ const Logo = () => (
7
+ <div className="flex items-center gap-2 font-montserrat font-bold text-2xl tracking-tight">
8
+ <img src="/logo.png" alt="Lytx logo" className="h-6 w-6" />
9
+ <span>Lytx</span>
10
+ </div>
11
+ );
12
+
13
+ const GitHubIcon = () => (
14
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
15
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
16
+ </svg>
17
+ );
18
+
19
+ const HamburgerIcon = () => (
20
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
21
+ <line x1="3" y1="6" x2="21" y2="6" />
22
+ <line x1="3" y1="12" x2="21" y2="12" />
23
+ <line x1="3" y1="18" x2="21" y2="18" />
24
+ </svg>
25
+ );
26
+
27
+ const CloseIcon = () => (
28
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
29
+ <line x1="18" y1="6" x2="6" y2="18" />
30
+ <line x1="6" y1="6" x2="18" y2="18" />
31
+ </svg>
32
+ );
33
+
34
+ type NavLink = { label: string; href: string };
35
+
36
+ export function MarketingNav({
37
+ links,
38
+ cta = { label: "Get Started", href: "/signup" },
39
+ }: {
40
+ links?: NavLink[];
41
+ cta?: NavLink;
42
+ }) {
43
+ const [isOpen, setIsOpen] = useState(false);
44
+
45
+ const navLinks = links ?? [
46
+ { label: "Features", href: "/#features" },
47
+ { label: "Get Started", href: "/signup" },
48
+ { label: "FAQ", href: "/#faq" },
49
+ ];
50
+
51
+ return (
52
+ <nav className="fixed w-full z-50 bg-slate-50/80 backdrop-blur-md border-b border-slate-200 dark:bg-black/80 dark:border-slate-800 transition-colors duration-300">
53
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
54
+ <div className="flex justify-between items-center h-16">
55
+ <div className="flex items-center gap-8">
56
+ <a href="/" className="flex-shrink-0 hover:opacity-80 transition-opacity">
57
+ <Logo />
58
+ </a>
59
+ <div className="hidden md:flex items-center space-x-6">
60
+ {navLinks.map((link) => (
61
+ <a
62
+ key={link.href}
63
+ href={link.href}
64
+ className="text-sm font-medium text-slate-600 hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400 transition-colors"
65
+ >
66
+ {link.label}
67
+ </a>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ <div className="flex items-center space-x-4">
72
+ <a
73
+ href="https://github.com/lytx-io/kit"
74
+ target="_blank"
75
+ rel="noreferrer"
76
+ className="hidden sm:inline-flex items-center gap-2 text-sm font-medium text-slate-600 hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400 transition-colors"
77
+ >
78
+ <GitHubIcon />
79
+ GitHub
80
+ </a>
81
+ <ThemeToggle />
82
+ <a
83
+ href="/login"
84
+ className="hidden sm:inline-flex text-sm font-medium text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white transition-colors"
85
+ >
86
+ Log in
87
+ </a>
88
+ <a
89
+ href={cta.href}
90
+ className="hidden sm:inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-full text-white bg-slate-900 hover:bg-slate-800 dark:bg-white dark:text-black dark:hover:bg-slate-200 transition-all shadow-sm"
91
+ >
92
+ {cta.label}
93
+ </a>
94
+
95
+ {/* Mobile hamburger */}
96
+ <button
97
+ onClick={() => setIsOpen(!isOpen)}
98
+ className="md:hidden p-2 text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white transition-colors"
99
+ aria-label="Toggle menu"
100
+ aria-expanded={isOpen}
101
+ >
102
+ {isOpen ? <CloseIcon /> : <HamburgerIcon />}
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ {/* Mobile menu */}
109
+ {isOpen && (
110
+ <div className="md:hidden bg-slate-50 dark:bg-black border-t border-slate-200 dark:border-slate-800">
111
+ <div className="px-4 py-4 space-y-3">
112
+ {navLinks.map((link) => (
113
+ <a
114
+ key={link.href}
115
+ href={link.href}
116
+ onClick={() => setIsOpen(false)}
117
+ className="block text-base font-medium text-slate-600 hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400 transition-colors py-2"
118
+ >
119
+ {link.label}
120
+ </a>
121
+ ))}
122
+ <div className="pt-3 border-t border-slate-200 dark:border-slate-800 space-y-3">
123
+ <a
124
+ href="https://github.com/lytx-io/kit"
125
+ target="_blank"
126
+ rel="noreferrer"
127
+ className="flex items-center gap-2 text-base font-medium text-slate-600 hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400 transition-colors py-2"
128
+ >
129
+ <GitHubIcon />
130
+ GitHub
131
+ </a>
132
+ <a
133
+ href="/login"
134
+ className="block text-base font-medium text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white transition-colors py-2"
135
+ >
136
+ Log in
137
+ </a>
138
+ <a
139
+ href={cta.href}
140
+ className="block text-center w-full px-4 py-3 text-base font-medium rounded-full text-white bg-slate-900 hover:bg-slate-800 dark:bg-white dark:text-black dark:hover:bg-slate-200 transition-all shadow-sm"
141
+ >
142
+ {cta.label}
143
+ </a>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ )}
148
+ </nav>
149
+ );
150
+ }