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.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState, type ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
DatePicker,
|
|
6
|
+
FilterModal,
|
|
7
|
+
HelpTooltip,
|
|
8
|
+
SpinnerIcon,
|
|
9
|
+
type DashboardFilters,
|
|
10
|
+
type DashboardNotice,
|
|
11
|
+
type DateRange,
|
|
12
|
+
} from "@/app/components/charts/ChartComponents";
|
|
13
|
+
|
|
14
|
+
type UseDashboardToolbarControlsParams = {
|
|
15
|
+
filters: DashboardFilters;
|
|
16
|
+
setFilters: React.Dispatch<React.SetStateAction<DashboardFilters>>;
|
|
17
|
+
timezone: string;
|
|
18
|
+
onNotify?: (notice: DashboardNotice) => void;
|
|
19
|
+
isUpdating?: boolean;
|
|
20
|
+
deviceTypeOptions?: string[];
|
|
21
|
+
countryOptions?: string[];
|
|
22
|
+
cityOptions?: string[];
|
|
23
|
+
regionOptions?: string[];
|
|
24
|
+
sourceOptions?: string[];
|
|
25
|
+
pageUrlOptions?: string[];
|
|
26
|
+
eventNameOptions?: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type DashboardToolbarControlsResult = {
|
|
30
|
+
controls: ReactNode;
|
|
31
|
+
footer: ReactNode;
|
|
32
|
+
modal: ReactNode;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getDateRangeDisplay = (dateRange: DateRange) => {
|
|
36
|
+
if (!dateRange.start || !dateRange.end) {
|
|
37
|
+
return "Loading dates...";
|
|
38
|
+
}
|
|
39
|
+
if (dateRange.preset) {
|
|
40
|
+
return dateRange.preset;
|
|
41
|
+
}
|
|
42
|
+
return `${dateRange.start} to ${dateRange.end}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function useDashboardToolbarControls({
|
|
46
|
+
filters,
|
|
47
|
+
setFilters,
|
|
48
|
+
timezone,
|
|
49
|
+
onNotify,
|
|
50
|
+
isUpdating = false,
|
|
51
|
+
deviceTypeOptions = [],
|
|
52
|
+
countryOptions = [],
|
|
53
|
+
cityOptions = [],
|
|
54
|
+
regionOptions = [],
|
|
55
|
+
sourceOptions = [],
|
|
56
|
+
pageUrlOptions = [],
|
|
57
|
+
eventNameOptions = [],
|
|
58
|
+
}: UseDashboardToolbarControlsParams): DashboardToolbarControlsResult {
|
|
59
|
+
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
|
60
|
+
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
|
61
|
+
|
|
62
|
+
const handleFiltersChange = useCallback(
|
|
63
|
+
(nextFilters: DashboardFilters) => {
|
|
64
|
+
setFilters(nextFilters);
|
|
65
|
+
|
|
66
|
+
const hasAnyFilter =
|
|
67
|
+
!!nextFilters.deviceType ||
|
|
68
|
+
!!nextFilters.country ||
|
|
69
|
+
!!nextFilters.city ||
|
|
70
|
+
!!nextFilters.region ||
|
|
71
|
+
!!nextFilters.source ||
|
|
72
|
+
!!nextFilters.pageUrl ||
|
|
73
|
+
!!nextFilters.eventName;
|
|
74
|
+
|
|
75
|
+
onNotify?.({
|
|
76
|
+
type: "info",
|
|
77
|
+
message: hasAnyFilter ? "Filters updated." : "All filters cleared.",
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
[onNotify, setFilters],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleDateRangeChange = useCallback(
|
|
84
|
+
(nextDateRange: DateRange) => {
|
|
85
|
+
setFilters((prev) => ({ ...prev, dateRange: nextDateRange }));
|
|
86
|
+
onNotify?.({ type: "info", message: "Date range updated." });
|
|
87
|
+
},
|
|
88
|
+
[onNotify, setFilters],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const controls = useMemo<ReactNode>(
|
|
92
|
+
() => (
|
|
93
|
+
<>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => setIsFilterModalOpen(true)}
|
|
97
|
+
aria-haspopup="dialog"
|
|
98
|
+
aria-expanded={isFilterModalOpen}
|
|
99
|
+
className="bg-(--theme-bg-secondary) hover:bg-(--theme-bg-tertiary) text-(--theme-text-primary) font-medium h-9 sm:h-auto px-2.5 sm:py-2 sm:px-4 text-sm rounded-md border border-(--theme-border-primary) transition-colors whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
|
|
100
|
+
>
|
|
101
|
+
Filter
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<div className="relative flex items-center gap-2">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => setIsDatePickerOpen((prev) => !prev)}
|
|
108
|
+
aria-haspopup="dialog"
|
|
109
|
+
aria-expanded={isDatePickerOpen}
|
|
110
|
+
className="bg-(--theme-bg-secondary) hover:bg-(--theme-bg-tertiary) text-(--theme-text-primary) font-medium h-9 sm:h-auto px-2.5 sm:py-2 sm:px-4 text-sm rounded-md border border-(--theme-border-primary) cursor-pointer transition-colors flex items-center gap-1.5 sm:gap-2 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
|
|
111
|
+
>
|
|
112
|
+
<span className="truncate max-w-32 sm:max-w-56">{getDateRangeDisplay(filters.dateRange)}</span>
|
|
113
|
+
<svg
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
focusable="false"
|
|
116
|
+
className="w-4 h-4 shrink-0"
|
|
117
|
+
fill="none"
|
|
118
|
+
stroke="currentColor"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
>
|
|
121
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
122
|
+
</svg>
|
|
123
|
+
</button>
|
|
124
|
+
|
|
125
|
+
<div className="hidden sm:flex">
|
|
126
|
+
<HelpTooltip text="Change the reporting period. Presets update immediately; custom dates apply when the picker closes." />
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<DatePicker
|
|
130
|
+
dateRange={filters.dateRange}
|
|
131
|
+
onDateRangeChange={handleDateRangeChange}
|
|
132
|
+
isOpen={isDatePickerOpen}
|
|
133
|
+
onToggle={() => setIsDatePickerOpen((prev) => !prev)}
|
|
134
|
+
timezone={timezone}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</>
|
|
138
|
+
),
|
|
139
|
+
[filters.dateRange, handleDateRangeChange, isDatePickerOpen, isFilterModalOpen, timezone],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const footer = useMemo<ReactNode>(
|
|
143
|
+
() => {
|
|
144
|
+
const chips: Array<{ label: string; key: keyof DashboardFilters }> = [];
|
|
145
|
+
if (filters.deviceType) chips.push({ label: `Device: ${filters.deviceType}`, key: "deviceType" });
|
|
146
|
+
if (filters.country) chips.push({ label: `Country: ${filters.country}`, key: "country" });
|
|
147
|
+
if (filters.region) chips.push({ label: `Region: ${filters.region}`, key: "region" });
|
|
148
|
+
if (filters.city) chips.push({ label: `City: ${filters.city}`, key: "city" });
|
|
149
|
+
if (filters.source) chips.push({ label: `Source: ${filters.source}`, key: "source" });
|
|
150
|
+
if (filters.pageUrl) chips.push({ label: `Page: ${filters.pageUrl}`, key: "pageUrl" });
|
|
151
|
+
if (filters.eventName) chips.push({ label: `Event: ${filters.eventName}`, key: "eventName" });
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
{chips.length > 0 ? (
|
|
156
|
+
<div className="flex flex-wrap items-center gap-1.5 mt-2">
|
|
157
|
+
{chips.map((chip) => (
|
|
158
|
+
<span
|
|
159
|
+
key={chip.key}
|
|
160
|
+
className="inline-flex items-center gap-1 rounded-full bg-(--theme-bg-tertiary) border border-(--theme-border-primary) px-2.5 py-0.5 text-xs text-(--theme-text-primary)"
|
|
161
|
+
>
|
|
162
|
+
{chip.label}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={() => setFilters((prev) => ({ ...prev, [chip.key]: undefined }))}
|
|
166
|
+
className="ml-0.5 rounded-full p-0.5 hover:bg-(--theme-bg-secondary) transition-colors focus:outline-none focus:ring-1 focus:ring-(--theme-border-secondary)"
|
|
167
|
+
aria-label={`Remove ${chip.label} filter`}
|
|
168
|
+
>
|
|
169
|
+
<svg aria-hidden="true" className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
170
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
171
|
+
</svg>
|
|
172
|
+
</button>
|
|
173
|
+
</span>
|
|
174
|
+
))}
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={() =>
|
|
178
|
+
setFilters((prev) => ({
|
|
179
|
+
dateRange: prev.dateRange,
|
|
180
|
+
siteId: prev.siteId,
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs text-(--theme-text-secondary) hover:text-(--theme-text-primary) transition-colors focus:outline-none focus:ring-1 focus:ring-(--theme-border-secondary)"
|
|
184
|
+
>
|
|
185
|
+
Clear all
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
|
|
190
|
+
{isUpdating ? (
|
|
191
|
+
<div className="flex items-center gap-2 mt-2 text-xs text-(--theme-text-secondary)">
|
|
192
|
+
<SpinnerIcon className="w-3 h-3 animate-spin" />
|
|
193
|
+
<span>Updating dashboard...</span>
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
[filters, isUpdating, setFilters],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const modal = useMemo<ReactNode>(
|
|
203
|
+
() => (
|
|
204
|
+
<FilterModal
|
|
205
|
+
filters={filters}
|
|
206
|
+
onFiltersChange={handleFiltersChange}
|
|
207
|
+
isOpen={isFilterModalOpen}
|
|
208
|
+
onClose={() => setIsFilterModalOpen(false)}
|
|
209
|
+
onNotify={(notice) => onNotify?.(notice)}
|
|
210
|
+
deviceTypeOptions={deviceTypeOptions}
|
|
211
|
+
countryOptions={countryOptions}
|
|
212
|
+
cityOptions={cityOptions}
|
|
213
|
+
regionOptions={regionOptions}
|
|
214
|
+
sourceOptions={sourceOptions}
|
|
215
|
+
pageUrlOptions={pageUrlOptions}
|
|
216
|
+
eventNameOptions={eventNameOptions}
|
|
217
|
+
/>
|
|
218
|
+
),
|
|
219
|
+
[
|
|
220
|
+
cityOptions,
|
|
221
|
+
countryOptions,
|
|
222
|
+
deviceTypeOptions,
|
|
223
|
+
eventNameOptions,
|
|
224
|
+
filters,
|
|
225
|
+
handleFiltersChange,
|
|
226
|
+
isFilterModalOpen,
|
|
227
|
+
onNotify,
|
|
228
|
+
pageUrlOptions,
|
|
229
|
+
regionOptions,
|
|
230
|
+
sourceOptions,
|
|
231
|
+
],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return { controls, footer, modal };
|
|
235
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
type AlertTone = "success" | "error" | "info" | "warning";
|
|
2
|
+
|
|
3
|
+
type AlertToneConfig = {
|
|
4
|
+
label: string;
|
|
5
|
+
borderClass: string;
|
|
6
|
+
bgClass: string;
|
|
7
|
+
textClass: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type AlertOverrides = Partial<AlertToneConfig>;
|
|
11
|
+
|
|
12
|
+
type AlertBannerProps = {
|
|
13
|
+
tone?: AlertTone;
|
|
14
|
+
title?: string;
|
|
15
|
+
message: string;
|
|
16
|
+
onDismiss?: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
accent?: AlertOverrides;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const toneStyles: Record<AlertTone, AlertToneConfig> = {
|
|
22
|
+
success: {
|
|
23
|
+
label: "Success",
|
|
24
|
+
borderClass: "border-emerald-500/60",
|
|
25
|
+
bgClass: "bg-emerald-500/10",
|
|
26
|
+
textClass: "text-emerald-400",
|
|
27
|
+
},
|
|
28
|
+
error: {
|
|
29
|
+
label: "Error",
|
|
30
|
+
borderClass: "border-red-500/70",
|
|
31
|
+
bgClass: "bg-red-500/10",
|
|
32
|
+
textClass: "text-red-400",
|
|
33
|
+
},
|
|
34
|
+
warning: {
|
|
35
|
+
label: "Warning",
|
|
36
|
+
borderClass: "border-amber-500/60",
|
|
37
|
+
bgClass: "bg-amber-500/10",
|
|
38
|
+
textClass: "text-amber-400",
|
|
39
|
+
},
|
|
40
|
+
info: {
|
|
41
|
+
label: "Info",
|
|
42
|
+
borderClass: "border-[var(--theme-border-primary)]",
|
|
43
|
+
bgClass: "bg-[var(--theme-bg-secondary)]",
|
|
44
|
+
textClass: "text-[var(--theme-text-primary)]",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function AlertBanner({
|
|
49
|
+
tone = "info",
|
|
50
|
+
title,
|
|
51
|
+
message,
|
|
52
|
+
onDismiss,
|
|
53
|
+
className,
|
|
54
|
+
accent,
|
|
55
|
+
}: AlertBannerProps) {
|
|
56
|
+
const base = toneStyles[tone];
|
|
57
|
+
const label = title ?? accent?.label ?? base.label;
|
|
58
|
+
const borderClass = accent?.borderClass ?? base.borderClass;
|
|
59
|
+
const bgClass = accent?.bgClass ?? base.bgClass;
|
|
60
|
+
const textClass = accent?.textClass ?? base.textClass;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
role="status"
|
|
65
|
+
className={`flex items-start justify-between gap-4 p-4 rounded-md border ${borderClass} ${bgClass} ${className ?? ""}`}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex-1">
|
|
68
|
+
<p className={`text-sm font-semibold ${textClass}`}>{label}</p>
|
|
69
|
+
<p className="text-sm text-[var(--theme-text-secondary)]">
|
|
70
|
+
{message}
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
{onDismiss ? (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={onDismiss}
|
|
77
|
+
className="text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--theme-border-secondary)] rounded"
|
|
78
|
+
aria-label="Dismiss notification"
|
|
79
|
+
>
|
|
80
|
+
<svg
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
focusable="false"
|
|
83
|
+
className="w-5 h-5"
|
|
84
|
+
fill="none"
|
|
85
|
+
stroke="currentColor"
|
|
86
|
+
viewBox="0 0 24 24"
|
|
87
|
+
>
|
|
88
|
+
<path
|
|
89
|
+
strokeLinecap="round"
|
|
90
|
+
strokeLinejoin="round"
|
|
91
|
+
strokeWidth={2}
|
|
92
|
+
d="M6 18L18 6M6 6l12 12"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type { AlertTone, AlertBannerProps };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
|
5
|
+
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
isLoading?: boolean;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const buttonVariants = {
|
|
11
|
+
primary: 'bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white border-transparent',
|
|
12
|
+
secondary: 'bg-[var(--color-secondary)] hover:bg-[var(--color-secondary-hover)] text-white border-transparent',
|
|
13
|
+
outline: 'bg-transparent hover:bg-[var(--theme-bg-secondary)] text-[var(--theme-text-primary)] border-[var(--theme-border-primary)]',
|
|
14
|
+
ghost: 'bg-transparent hover:bg-[var(--theme-bg-secondary)] text-[var(--theme-text-primary)] border-transparent',
|
|
15
|
+
danger: 'bg-[var(--color-danger)] hover:bg-[var(--color-danger-hover)] text-white border-transparent'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const buttonSizes = {
|
|
19
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
20
|
+
md: 'px-4 py-2 text-base',
|
|
21
|
+
lg: 'px-6 py-3 text-lg'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Button: React.FC<ButtonProps> = ({
|
|
25
|
+
variant = 'primary',
|
|
26
|
+
size = 'md',
|
|
27
|
+
isLoading = false,
|
|
28
|
+
disabled,
|
|
29
|
+
className = '',
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}) => {
|
|
33
|
+
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg border cursor-pointer transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed';
|
|
34
|
+
|
|
35
|
+
const variantClasses = buttonVariants[variant];
|
|
36
|
+
const sizeClasses = buttonSizes[size];
|
|
37
|
+
|
|
38
|
+
const combinedClasses = `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`.trim();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
className={combinedClasses}
|
|
43
|
+
disabled={disabled || isLoading}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{isLoading && (
|
|
47
|
+
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
48
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
49
|
+
<path className="opacity-75" fill="currentColor" 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"></path>
|
|
50
|
+
</svg>
|
|
51
|
+
)}
|
|
52
|
+
{children}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface CardProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
7
|
+
shadow?: 'none' | 'sm' | 'md' | 'lg';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const paddingVariants = {
|
|
11
|
+
none: '',
|
|
12
|
+
sm: 'p-3',
|
|
13
|
+
md: 'p-4',
|
|
14
|
+
lg: 'p-6'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const shadowVariants = {
|
|
18
|
+
none: '',
|
|
19
|
+
sm: 'shadow-[var(--shadow-sm)]',
|
|
20
|
+
md: 'shadow-[var(--shadow-md)]',
|
|
21
|
+
lg: 'shadow-[var(--shadow-lg)]'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Card: React.FC<CardProps> = ({
|
|
25
|
+
children,
|
|
26
|
+
className = '',
|
|
27
|
+
padding = 'md',
|
|
28
|
+
shadow = 'sm'
|
|
29
|
+
}) => {
|
|
30
|
+
const baseClasses = 'bg-[var(--theme-card-bg)] border border-[var(--theme-card-border)] rounded-lg';
|
|
31
|
+
const paddingClasses = paddingVariants[padding];
|
|
32
|
+
const shadowClasses = shadowVariants[shadow];
|
|
33
|
+
|
|
34
|
+
const combinedClasses = `${baseClasses} ${paddingClasses} ${shadowClasses} ${className}`.trim();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={combinedClasses}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface CardHeaderProps {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const CardHeader: React.FC<CardHeaderProps> = ({ children, className = '' }) => {
|
|
49
|
+
return (
|
|
50
|
+
<div className={`border-b border-[var(--theme-border-primary)] pb-3 mb-4 ${className}`}>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export interface CardTitleProps {
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const CardTitle: React.FC<CardTitleProps> = ({ children, className = '' }) => {
|
|
62
|
+
return (
|
|
63
|
+
<h3 className={`text-lg font-semibold text-[var(--theme-text-primary)] ${className}`}>
|
|
64
|
+
{children}
|
|
65
|
+
</h3>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export interface CardContentProps {
|
|
70
|
+
children: React.ReactNode;
|
|
71
|
+
className?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const CardContent: React.FC<CardContentProps> = ({ children, className = '' }) => {
|
|
75
|
+
return (
|
|
76
|
+
<div className={className}>
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useId, forwardRef } from "react";
|
|
2
|
+
|
|
3
|
+
export interface InputProps
|
|
4
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
label?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
helperText?: string;
|
|
8
|
+
variant?: "default" | "filled";
|
|
9
|
+
inputSize?: "sm" | "md" | "lg";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const inputSizes = {
|
|
13
|
+
sm: "px-3 py-1.5 text-sm",
|
|
14
|
+
md: "px-4 py-2 text-base",
|
|
15
|
+
lg: "px-4 py-3 text-lg",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>((
|
|
19
|
+
{
|
|
20
|
+
label,
|
|
21
|
+
error,
|
|
22
|
+
helperText,
|
|
23
|
+
variant = "default",
|
|
24
|
+
inputSize = "md",
|
|
25
|
+
className = "",
|
|
26
|
+
id,
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref) => {
|
|
30
|
+
const generatedId = useId();
|
|
31
|
+
const inputId = id || generatedId;
|
|
32
|
+
const baseClasses =
|
|
33
|
+
"w-full rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed";
|
|
34
|
+
|
|
35
|
+
const variantClasses =
|
|
36
|
+
variant === "filled"
|
|
37
|
+
? "bg-[var(--theme-bg-secondary)] border-transparent"
|
|
38
|
+
: "bg-[var(--theme-input-bg)] border-[var(--theme-input-border)]";
|
|
39
|
+
|
|
40
|
+
const errorClasses = error
|
|
41
|
+
? "border-[var(--color-danger)] focus:ring-[var(--color-danger)] focus:border-[var(--color-danger)]"
|
|
42
|
+
: "";
|
|
43
|
+
|
|
44
|
+
const sizeClasses = inputSizes[inputSize];
|
|
45
|
+
|
|
46
|
+
const combinedClasses =
|
|
47
|
+
`${baseClasses} ${variantClasses} ${errorClasses} ${sizeClasses} ${className}`.trim();
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="w-full">
|
|
51
|
+
{label && (
|
|
52
|
+
<label
|
|
53
|
+
htmlFor={inputId}
|
|
54
|
+
className="block text-sm font-medium text-[var(--theme-text-primary)] mb-2"
|
|
55
|
+
>
|
|
56
|
+
{label}
|
|
57
|
+
</label>
|
|
58
|
+
)}
|
|
59
|
+
<input ref={ref} id={inputId} className={combinedClasses} {...props} />
|
|
60
|
+
{error && (
|
|
61
|
+
<p className="mt-1 text-sm text-[var(--color-danger)]">{error}</p>
|
|
62
|
+
)}
|
|
63
|
+
{helperText && !error && (
|
|
64
|
+
<p className="mt-1 text-sm text-[var(--theme-text-secondary)]">
|
|
65
|
+
{helperText}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
//For development only
|
|
72
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { AnchorHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
5
|
+
href: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A semantic Link component for internal navigation.
|
|
11
|
+
* View transitions are handled globally in client.tsx for all navigation.
|
|
12
|
+
*/
|
|
13
|
+
export function Link({
|
|
14
|
+
href,
|
|
15
|
+
children,
|
|
16
|
+
...props
|
|
17
|
+
}: LinkProps) {
|
|
18
|
+
return (
|
|
19
|
+
<a href={href} {...props}>
|
|
20
|
+
{children}
|
|
21
|
+
</a>
|
|
22
|
+
);
|
|
23
|
+
}
|