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,755 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type FormEvent, useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { AuthContext, signOut } from "@/app/providers/AuthProvider";
|
|
4
|
+
import { ThemeToggle } from "./ui/ThemeToggle";
|
|
5
|
+
import { Link } from "./ui/Link";
|
|
6
|
+
|
|
7
|
+
type TeamInfo = {
|
|
8
|
+
id: number;
|
|
9
|
+
name?: string | null;
|
|
10
|
+
external_id?: number | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type NavInitialSession = {
|
|
14
|
+
user?: {
|
|
15
|
+
name?: string | null;
|
|
16
|
+
email?: string | null;
|
|
17
|
+
image?: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
team?: TeamInfo | null;
|
|
20
|
+
all_teams?: TeamInfo[] | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const LAST_TEAM_KEY = "lytx_last_team_id";
|
|
24
|
+
|
|
25
|
+
const getLastTeamFromStorage = (): number | null => {
|
|
26
|
+
try {
|
|
27
|
+
const stored = localStorage.getItem(LAST_TEAM_KEY);
|
|
28
|
+
return stored ? Number.parseInt(stored, 10) : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const saveLastTeamToStorage = (teamId: number): void => {
|
|
35
|
+
try {
|
|
36
|
+
localStorage.setItem(LAST_TEAM_KEY, String(teamId));
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore storage errors
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type NavProps = {
|
|
43
|
+
initialSession?: NavInitialSession | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function Nav({ initialSession = null }: NavProps) {
|
|
47
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
48
|
+
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
|
49
|
+
const [isTeamMenuOpen, setIsTeamMenuOpen] = useState(false);
|
|
50
|
+
const [isCreateTeamOpen, setIsCreateTeamOpen] = useState(false);
|
|
51
|
+
const [createTeamName, setCreateTeamName] = useState("");
|
|
52
|
+
const [createTeamError, setCreateTeamError] = useState("");
|
|
53
|
+
const [isCreatingTeam, setIsCreatingTeam] = useState(false);
|
|
54
|
+
const [switchingTeamId, setSwitchingTeamId] = useState<number | null>(null);
|
|
55
|
+
const [activeTeamId, setActiveTeamId] = useState<number | null>(null);
|
|
56
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
const { data: authSession, refetch, setCurrentSite } = useContext(AuthContext);
|
|
59
|
+
const session = authSession ?? initialSession;
|
|
60
|
+
const user = session?.user as { name?: string | null; email?: string | null; image?: string | null } | undefined;
|
|
61
|
+
const sessionTeamId = session?.team?.id ?? null;
|
|
62
|
+
const rawTeams = (session?.all_teams ?? []) as TeamInfo[];
|
|
63
|
+
const teamMap = new Map<number, TeamInfo>();
|
|
64
|
+
for (const team of rawTeams) {
|
|
65
|
+
if (team?.id != null) teamMap.set(team.id, team);
|
|
66
|
+
}
|
|
67
|
+
if (!teamMap.size && session?.team?.id) {
|
|
68
|
+
teamMap.set(session.team.id, {
|
|
69
|
+
id: session.team.id,
|
|
70
|
+
name: session.team.name,
|
|
71
|
+
external_id: session.team.external_id,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const teamList = Array.from(teamMap.values()).toSorted((a, b) => {
|
|
75
|
+
if (a.id === sessionTeamId) return -1;
|
|
76
|
+
if (b.id === sessionTeamId) return 1;
|
|
77
|
+
return (a.name || "").localeCompare(b.name || "");
|
|
78
|
+
});
|
|
79
|
+
const userName = user?.name?.trim() || "";
|
|
80
|
+
const userEmail = user?.email?.trim() || "";
|
|
81
|
+
const userImage = user?.image || "";
|
|
82
|
+
|
|
83
|
+
const getInitials = (name: string, email: string) => {
|
|
84
|
+
if (name) {
|
|
85
|
+
const parts = name.split(" ").filter(Boolean);
|
|
86
|
+
const first = parts[0]?.[0] || "";
|
|
87
|
+
const last = parts.length > 1 ? parts[parts.length - 1]?.[0] : "";
|
|
88
|
+
const initials = `${first}${last}` || first;
|
|
89
|
+
return initials.toUpperCase() || "U";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (email) {
|
|
93
|
+
const localPart = email.split("@")[0] || "";
|
|
94
|
+
const letters = localPart.replace(/[^a-zA-Z0-9]/g, "");
|
|
95
|
+
return (letters.slice(0, 2) || "U").toUpperCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return "U";
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const userInitials = getInitials(userName, userEmail);
|
|
102
|
+
const resolvedTeamId =
|
|
103
|
+
activeTeamId && teamList.some((team) => team.id === activeTeamId)
|
|
104
|
+
? activeTeamId
|
|
105
|
+
: sessionTeamId;
|
|
106
|
+
const currentTeam =
|
|
107
|
+
teamList.find((team) => team.id === resolvedTeamId) ?? teamList[0] ?? null;
|
|
108
|
+
const otherTeams = teamList.filter((team) => team.id !== resolvedTeamId);
|
|
109
|
+
const teamButtonBaseClass =
|
|
110
|
+
"flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-2 text-left text-sm text-[var(--theme-text-primary)] transition-colors disabled:cursor-not-allowed disabled:opacity-60";
|
|
111
|
+
|
|
112
|
+
const renderTeamButton = (team: TeamInfo, isCurrent: boolean) => {
|
|
113
|
+
const isSwitching = switchingTeamId === team.id;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
key={team.id}
|
|
117
|
+
type="button"
|
|
118
|
+
className={`${teamButtonBaseClass} ${isCurrent
|
|
119
|
+
? "bg-[var(--theme-bg-secondary)]"
|
|
120
|
+
: "hover:text-[var(--color-primary)]"
|
|
121
|
+
}`}
|
|
122
|
+
aria-current={isCurrent ? "true" : undefined}
|
|
123
|
+
onClick={() => handleSwitchTeam(team.id)}
|
|
124
|
+
disabled={switchingTeamId !== null}
|
|
125
|
+
>
|
|
126
|
+
<span className="truncate">{team.name || "Untitled team"}</span>
|
|
127
|
+
{isCurrent ? (
|
|
128
|
+
<svg
|
|
129
|
+
width="14"
|
|
130
|
+
height="14"
|
|
131
|
+
viewBox="0 0 24 24"
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
strokeWidth="2"
|
|
135
|
+
strokeLinecap="round"
|
|
136
|
+
strokeLinejoin="round"
|
|
137
|
+
className="text-[var(--color-primary)]"
|
|
138
|
+
aria-hidden="true"
|
|
139
|
+
>
|
|
140
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
141
|
+
</svg>
|
|
142
|
+
) : isSwitching ? (
|
|
143
|
+
<span className="text-[10px] text-[var(--theme-text-secondary)]">Switching</span>
|
|
144
|
+
) : null}
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const openCreateTeamModal = () => {
|
|
150
|
+
setIsCreateTeamOpen(true);
|
|
151
|
+
setCreateTeamError("");
|
|
152
|
+
setIsUserMenuOpen(false);
|
|
153
|
+
setIsMenuOpen(false);
|
|
154
|
+
setIsTeamMenuOpen(false);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const closeCreateTeamModal = () => {
|
|
158
|
+
setIsCreateTeamOpen(false);
|
|
159
|
+
setCreateTeamError("");
|
|
160
|
+
setCreateTeamName("");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleCreateTeam = async (event: FormEvent<HTMLFormElement>) => {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
if (isCreatingTeam) return;
|
|
166
|
+
const trimmedName = createTeamName.trim();
|
|
167
|
+
if (!trimmedName) {
|
|
168
|
+
setCreateTeamError("Team name is required");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (trimmedName.length < 2) {
|
|
172
|
+
setCreateTeamError("Team name must be at least 2 characters");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (trimmedName.length > 80) {
|
|
176
|
+
setCreateTeamError("Team name must be 80 characters or less");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setIsCreatingTeam(true);
|
|
181
|
+
setCreateTeamError("");
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch("/api/team/create", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "Content-Type": "application/json" },
|
|
187
|
+
body: JSON.stringify({ name: trimmedName }),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const data = (await response.json().catch(() => null)) as
|
|
191
|
+
| { error?: string; team_id?: number }
|
|
192
|
+
| null;
|
|
193
|
+
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw new Error(data?.error || "Failed to create team");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
closeCreateTeamModal();
|
|
199
|
+
|
|
200
|
+
if (data?.team_id) {
|
|
201
|
+
await handleSwitchTeam(data.team_id);
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const message = error instanceof Error ? error.message : "Failed to create team";
|
|
205
|
+
setCreateTeamError(message);
|
|
206
|
+
} finally {
|
|
207
|
+
setIsCreatingTeam(false);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleSwitchTeam = async (teamId: number) => {
|
|
212
|
+
if (teamId === resolvedTeamId) {
|
|
213
|
+
setIsUserMenuOpen(false);
|
|
214
|
+
setIsMenuOpen(false);
|
|
215
|
+
setIsTeamMenuOpen(false);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (switchingTeamId) return;
|
|
219
|
+
|
|
220
|
+
setSwitchingTeamId(teamId);
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch("/api/user/update-last-team", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "Content-Type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ team_id: teamId }),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const data = (await response.json().catch(() => null)) as
|
|
229
|
+
| {
|
|
230
|
+
error?: string;
|
|
231
|
+
userSites?: Array<{ site_id: number; name?: string | null; tag_id: string }>;
|
|
232
|
+
}
|
|
233
|
+
| null;
|
|
234
|
+
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new Error(data?.error || "Failed to switch team");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setActiveTeamId(teamId);
|
|
240
|
+
saveLastTeamToStorage(teamId);
|
|
241
|
+
|
|
242
|
+
await refetch();
|
|
243
|
+
|
|
244
|
+
if (!data?.userSites || data.userSites.length === 0) {
|
|
245
|
+
window.location.assign("/dashboard/new-site");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const nextSite = data.userSites[0];
|
|
250
|
+
if (nextSite) {
|
|
251
|
+
setCurrentSite({
|
|
252
|
+
name: nextSite.name || "",
|
|
253
|
+
id: nextSite.site_id,
|
|
254
|
+
tag_id: nextSite.tag_id,
|
|
255
|
+
});
|
|
256
|
+
if (
|
|
257
|
+
window.location.pathname === "/dashboard/new-site" ||
|
|
258
|
+
window.location.pathname === "/new-site"
|
|
259
|
+
) {
|
|
260
|
+
window.location.assign("/dashboard");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
setCurrentSite(null);
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error("Error switching team:", error);
|
|
268
|
+
} finally {
|
|
269
|
+
setSwitchingTeamId(null);
|
|
270
|
+
setIsUserMenuOpen(false);
|
|
271
|
+
setIsMenuOpen(false);
|
|
272
|
+
setIsTeamMenuOpen(false);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!isUserMenuOpen) return;
|
|
278
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
279
|
+
const target = event.target as Node;
|
|
280
|
+
if (menuRef.current?.contains(target)) return;
|
|
281
|
+
if (mobileMenuRef.current?.contains(target)) return;
|
|
282
|
+
setIsUserMenuOpen(false);
|
|
283
|
+
};
|
|
284
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
285
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
286
|
+
}, [isUserMenuOpen]);
|
|
287
|
+
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (!isUserMenuOpen) setIsTeamMenuOpen(false);
|
|
290
|
+
}, [isUserMenuOpen]);
|
|
291
|
+
|
|
292
|
+
const hasReconciledTeam = useRef(false);
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (!teamList.length || hasReconciledTeam.current) return;
|
|
295
|
+
const storedTeamId = getLastTeamFromStorage();
|
|
296
|
+
if (storedTeamId && teamList.some((team) => team.id === storedTeamId)) {
|
|
297
|
+
if (storedTeamId !== sessionTeamId) {
|
|
298
|
+
hasReconciledTeam.current = true;
|
|
299
|
+
handleSwitchTeam(storedTeamId);
|
|
300
|
+
} else {
|
|
301
|
+
setActiveTeamId(storedTeamId);
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (sessionTeamId) {
|
|
306
|
+
setActiveTeamId(sessionTeamId);
|
|
307
|
+
saveLastTeamToStorage(sessionTeamId);
|
|
308
|
+
}
|
|
309
|
+
}, [sessionTeamId, teamList]);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<>
|
|
313
|
+
<nav className="flex justify-between items-center p-4 bg-[var(--theme-bg-primary)] border-b border-[var(--theme-border-primary)]">
|
|
314
|
+
<Link href="/dashboard" className="logo flex items-center gap-2">
|
|
315
|
+
<img src="/logo.png" alt="Lytx logo" className="h-6 w-6" />
|
|
316
|
+
<span className="text-xl font-bold text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors">
|
|
317
|
+
Lytx
|
|
318
|
+
</span>
|
|
319
|
+
</Link>
|
|
320
|
+
|
|
321
|
+
{/* Desktop navigation */}
|
|
322
|
+
<div className="hidden md:flex items-center gap-4">
|
|
323
|
+
<ul className="flex gap-6">
|
|
324
|
+
<li className="cursor-pointer">
|
|
325
|
+
<Link
|
|
326
|
+
href="/dashboard"
|
|
327
|
+
className="text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
328
|
+
>
|
|
329
|
+
Overview
|
|
330
|
+
</Link>
|
|
331
|
+
</li>
|
|
332
|
+
<li className="cursor-pointer">
|
|
333
|
+
<Link
|
|
334
|
+
href="/dashboard/events"
|
|
335
|
+
className="text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
336
|
+
>
|
|
337
|
+
Events
|
|
338
|
+
</Link>
|
|
339
|
+
</li>
|
|
340
|
+
<li className="cursor-pointer">
|
|
341
|
+
<Link
|
|
342
|
+
href="/dashboard/explore"
|
|
343
|
+
className="text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
344
|
+
>
|
|
345
|
+
Explore
|
|
346
|
+
</Link>
|
|
347
|
+
</li>
|
|
348
|
+
</ul>
|
|
349
|
+
<div className="relative" ref={menuRef}>
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
onClick={() => setIsUserMenuOpen((open) => !open)}
|
|
353
|
+
className="flex items-center gap-2 rounded-full border border-[var(--theme-border-primary)] bg-[var(--theme-bg-primary)] p-1 text-[var(--theme-text-primary)] hover:border-[var(--color-primary)] transition-colors"
|
|
354
|
+
aria-label="Open user menu"
|
|
355
|
+
aria-expanded={isUserMenuOpen}
|
|
356
|
+
>
|
|
357
|
+
{userImage ? (
|
|
358
|
+
<img
|
|
359
|
+
src={userImage}
|
|
360
|
+
alt={userName ? `${userName} avatar` : "User avatar"}
|
|
361
|
+
className="h-8 w-8 rounded-full object-cover"
|
|
362
|
+
/>
|
|
363
|
+
) : (
|
|
364
|
+
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[var(--theme-border-primary)] bg-[var(--theme-card-bg)] text-xs font-semibold text-[var(--theme-text-primary)]">
|
|
365
|
+
{userInitials}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
368
|
+
<svg
|
|
369
|
+
width="14"
|
|
370
|
+
height="14"
|
|
371
|
+
viewBox="0 0 24 24"
|
|
372
|
+
fill="none"
|
|
373
|
+
stroke="currentColor"
|
|
374
|
+
strokeWidth="2"
|
|
375
|
+
strokeLinecap="round"
|
|
376
|
+
strokeLinejoin="round"
|
|
377
|
+
className="opacity-70"
|
|
378
|
+
aria-hidden="true"
|
|
379
|
+
>
|
|
380
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
381
|
+
</svg>
|
|
382
|
+
</button>
|
|
383
|
+
{isUserMenuOpen && (
|
|
384
|
+
<div className="absolute right-0 mt-2 w-52 min-w-[13rem] rounded-md border border-[var(--theme-border-primary)] bg-[var(--theme-bg-primary)] shadow-lg z-[80]">
|
|
385
|
+
<div className="px-3 py-2 text-xs text-[var(--theme-text-secondary)]">
|
|
386
|
+
{userName || userEmail || "Signed in"}
|
|
387
|
+
</div>
|
|
388
|
+
{teamList.length > 0 && (
|
|
389
|
+
<div className="border-t border-[var(--theme-border-primary)] py-1">
|
|
390
|
+
<div
|
|
391
|
+
className="relative"
|
|
392
|
+
onMouseEnter={() => setIsTeamMenuOpen(true)}
|
|
393
|
+
onMouseLeave={() => setIsTeamMenuOpen(false)}
|
|
394
|
+
>
|
|
395
|
+
<span
|
|
396
|
+
className="pointer-events-none absolute -right-3 -top-2 h-[calc(100%+1rem)] w-3"
|
|
397
|
+
aria-hidden="true"
|
|
398
|
+
/>
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
className="flex w-full cursor-pointer items-center justify-between px-3 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
402
|
+
onClick={() => setIsTeamMenuOpen((open) => !open)}
|
|
403
|
+
aria-haspopup="menu"
|
|
404
|
+
aria-expanded={isTeamMenuOpen}
|
|
405
|
+
>
|
|
406
|
+
Switch Team
|
|
407
|
+
<svg
|
|
408
|
+
width="14"
|
|
409
|
+
height="14"
|
|
410
|
+
viewBox="0 0 24 24"
|
|
411
|
+
fill="none"
|
|
412
|
+
stroke="currentColor"
|
|
413
|
+
strokeWidth="2"
|
|
414
|
+
strokeLinecap="round"
|
|
415
|
+
strokeLinejoin="round"
|
|
416
|
+
className="opacity-60"
|
|
417
|
+
aria-hidden="true"
|
|
418
|
+
>
|
|
419
|
+
<polyline points="9 6 15 12 9 18"></polyline>
|
|
420
|
+
</svg>
|
|
421
|
+
</button>
|
|
422
|
+
<div
|
|
423
|
+
className={`absolute right-full -top-1 -mr-0.5 w-64 min-w-[16rem] rounded-md border border-[var(--theme-border-primary)] bg-[var(--theme-bg-primary)] shadow-lg z-[90] ${isTeamMenuOpen ? "block" : "hidden"
|
|
424
|
+
}`}
|
|
425
|
+
>
|
|
426
|
+
<div className="px-3 pt-2 text-[11px] uppercase tracking-wide text-[var(--theme-text-secondary)]">
|
|
427
|
+
Current team
|
|
428
|
+
</div>
|
|
429
|
+
{currentTeam ? renderTeamButton(currentTeam, true) : null}
|
|
430
|
+
{otherTeams.length > 0 && (
|
|
431
|
+
<div className="border-t border-[var(--theme-border-primary)] mt-1 pt-1">
|
|
432
|
+
<div className="px-3 pt-2 text-[11px] uppercase tracking-wide text-[var(--theme-text-secondary)]">
|
|
433
|
+
Other teams
|
|
434
|
+
</div>
|
|
435
|
+
{otherTeams.map((team) => renderTeamButton(team, false))}
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
<div className="border-t border-[var(--theme-border-primary)] mt-1 pt-1">
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
|
442
|
+
onClick={openCreateTeamModal}
|
|
443
|
+
disabled={isCreatingTeam || switchingTeamId !== null}
|
|
444
|
+
>
|
|
445
|
+
<span className="flex h-5 w-5 items-center justify-center rounded-full border border-[var(--theme-border-primary)] text-xs">
|
|
446
|
+
+
|
|
447
|
+
</span>
|
|
448
|
+
Create a team
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
<Link
|
|
456
|
+
href="/dashboard/settings"
|
|
457
|
+
className="block cursor-pointer px-3 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
458
|
+
onClick={() => setIsUserMenuOpen(false)}
|
|
459
|
+
>
|
|
460
|
+
Settings
|
|
461
|
+
</Link>
|
|
462
|
+
<div className="flex items-center justify-between px-3 py-2 text-sm text-[var(--theme-text-primary)]">
|
|
463
|
+
<span>Theme</span>
|
|
464
|
+
<ThemeToggle />
|
|
465
|
+
</div>
|
|
466
|
+
<button
|
|
467
|
+
className="w-full cursor-pointer px-3 py-2 text-left text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
468
|
+
onClick={() => {
|
|
469
|
+
setIsUserMenuOpen(false);
|
|
470
|
+
signOut();
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
Sign Out
|
|
474
|
+
</button>
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
{/* Mobile hamburger button */}
|
|
481
|
+
<div className="flex md:hidden items-center gap-2">
|
|
482
|
+
<button
|
|
483
|
+
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
484
|
+
className="p-2 text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
485
|
+
aria-label="Toggle menu"
|
|
486
|
+
>
|
|
487
|
+
{isMenuOpen ? (
|
|
488
|
+
<svg
|
|
489
|
+
width="24"
|
|
490
|
+
height="24"
|
|
491
|
+
viewBox="0 0 24 24"
|
|
492
|
+
fill="none"
|
|
493
|
+
stroke="currentColor"
|
|
494
|
+
strokeWidth="2"
|
|
495
|
+
strokeLinecap="round"
|
|
496
|
+
strokeLinejoin="round"
|
|
497
|
+
>
|
|
498
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
499
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
500
|
+
</svg>
|
|
501
|
+
) : (
|
|
502
|
+
<svg
|
|
503
|
+
width="24"
|
|
504
|
+
height="24"
|
|
505
|
+
viewBox="0 0 24 24"
|
|
506
|
+
fill="none"
|
|
507
|
+
stroke="currentColor"
|
|
508
|
+
strokeWidth="2"
|
|
509
|
+
strokeLinecap="round"
|
|
510
|
+
strokeLinejoin="round"
|
|
511
|
+
>
|
|
512
|
+
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
513
|
+
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
514
|
+
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
515
|
+
</svg>
|
|
516
|
+
)}
|
|
517
|
+
</button>
|
|
518
|
+
</div>
|
|
519
|
+
</nav>
|
|
520
|
+
|
|
521
|
+
{/* Mobile menu dropdown */}
|
|
522
|
+
{isMenuOpen && (
|
|
523
|
+
<div ref={mobileMenuRef} className="md:hidden bg-[var(--theme-bg-primary)] border-b border-[var(--theme-border-primary)] relative z-[80]">
|
|
524
|
+
<ul className="flex flex-col p-4 gap-4">
|
|
525
|
+
<li>
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
528
|
+
onClick={() => setIsUserMenuOpen((open) => !open)}
|
|
529
|
+
className="flex w-full items-center justify-between rounded-full border border-[var(--theme-border-primary)] bg-[var(--theme-bg-primary)] p-2 text-[var(--theme-text-primary)] hover:border-[var(--color-primary)] transition-colors"
|
|
530
|
+
aria-label="Open user menu"
|
|
531
|
+
aria-expanded={isUserMenuOpen}
|
|
532
|
+
>
|
|
533
|
+
<span className="flex items-center gap-2">
|
|
534
|
+
{userImage ? (
|
|
535
|
+
<img
|
|
536
|
+
src={userImage}
|
|
537
|
+
alt={userName ? `${userName} avatar` : "User avatar"}
|
|
538
|
+
className="h-8 w-8 rounded-full object-cover"
|
|
539
|
+
/>
|
|
540
|
+
) : (
|
|
541
|
+
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[var(--theme-border-primary)] bg-[var(--theme-card-bg)] text-xs font-semibold text-[var(--theme-text-primary)]">
|
|
542
|
+
{userInitials}
|
|
543
|
+
</span>
|
|
544
|
+
)}
|
|
545
|
+
<span className="text-sm">
|
|
546
|
+
{userName || userEmail || "Signed in"}
|
|
547
|
+
</span>
|
|
548
|
+
</span>
|
|
549
|
+
<svg
|
|
550
|
+
width="14"
|
|
551
|
+
height="14"
|
|
552
|
+
viewBox="0 0 24 24"
|
|
553
|
+
fill="none"
|
|
554
|
+
stroke="currentColor"
|
|
555
|
+
strokeWidth="2"
|
|
556
|
+
strokeLinecap="round"
|
|
557
|
+
strokeLinejoin="round"
|
|
558
|
+
className="opacity-70"
|
|
559
|
+
aria-hidden="true"
|
|
560
|
+
>
|
|
561
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
562
|
+
</svg>
|
|
563
|
+
</button>
|
|
564
|
+
{isUserMenuOpen && (
|
|
565
|
+
<div className="mt-2 rounded-md border border-[var(--theme-border-primary)] bg-[var(--theme-bg-primary)]">
|
|
566
|
+
{teamList.length > 0 && (
|
|
567
|
+
<div className="border-b border-[var(--theme-border-primary)]">
|
|
568
|
+
<button
|
|
569
|
+
type="button"
|
|
570
|
+
className="flex w-full cursor-pointer items-center justify-between px-3 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
571
|
+
onClick={() => setIsTeamMenuOpen((open) => !open)}
|
|
572
|
+
aria-haspopup="menu"
|
|
573
|
+
aria-expanded={isTeamMenuOpen}
|
|
574
|
+
>
|
|
575
|
+
Switch Team
|
|
576
|
+
<svg
|
|
577
|
+
width="14"
|
|
578
|
+
height="14"
|
|
579
|
+
viewBox="0 0 24 24"
|
|
580
|
+
fill="none"
|
|
581
|
+
stroke="currentColor"
|
|
582
|
+
strokeWidth="2"
|
|
583
|
+
strokeLinecap="round"
|
|
584
|
+
strokeLinejoin="round"
|
|
585
|
+
className={`opacity-60 transition-transform ${isTeamMenuOpen ? "rotate-180" : ""
|
|
586
|
+
}`}
|
|
587
|
+
aria-hidden="true"
|
|
588
|
+
>
|
|
589
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
590
|
+
</svg>
|
|
591
|
+
</button>
|
|
592
|
+
{isTeamMenuOpen && (
|
|
593
|
+
<div className="pb-2">
|
|
594
|
+
<div className="px-3 pt-2 text-[11px] uppercase tracking-wide text-[var(--theme-text-secondary)]">
|
|
595
|
+
Current team
|
|
596
|
+
</div>
|
|
597
|
+
{currentTeam ? renderTeamButton(currentTeam, true) : null}
|
|
598
|
+
{otherTeams.length > 0 && (
|
|
599
|
+
<div className="border-t border-[var(--theme-border-primary)] mt-1 pt-1">
|
|
600
|
+
<div className="px-3 pt-2 text-[11px] uppercase tracking-wide text-[var(--theme-text-secondary)]">
|
|
601
|
+
Other teams
|
|
602
|
+
</div>
|
|
603
|
+
{otherTeams.map((team) => renderTeamButton(team, false))}
|
|
604
|
+
</div>
|
|
605
|
+
)}
|
|
606
|
+
<div className="border-t border-[var(--theme-border-primary)] mt-1 pt-1">
|
|
607
|
+
<button
|
|
608
|
+
type="button"
|
|
609
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
|
610
|
+
onClick={openCreateTeamModal}
|
|
611
|
+
disabled={isCreatingTeam || switchingTeamId !== null}
|
|
612
|
+
>
|
|
613
|
+
<span className="flex h-5 w-5 items-center justify-center rounded-full border border-[var(--theme-border-primary)] text-xs">
|
|
614
|
+
+
|
|
615
|
+
</span>
|
|
616
|
+
Create a team
|
|
617
|
+
</button>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
<Link
|
|
624
|
+
href="/dashboard/settings"
|
|
625
|
+
className="block cursor-pointer px-3 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
626
|
+
onClick={() => {
|
|
627
|
+
setIsUserMenuOpen(false);
|
|
628
|
+
setIsMenuOpen(false);
|
|
629
|
+
}}
|
|
630
|
+
>
|
|
631
|
+
Settings
|
|
632
|
+
</Link>
|
|
633
|
+
<div className="flex items-center justify-between px-3 py-2 text-sm text-[var(--theme-text-primary)]">
|
|
634
|
+
<span>Theme</span>
|
|
635
|
+
<ThemeToggle />
|
|
636
|
+
</div>
|
|
637
|
+
<button
|
|
638
|
+
className="w-full cursor-pointer px-3 py-2 text-left text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
639
|
+
onClick={() => {
|
|
640
|
+
setIsUserMenuOpen(false);
|
|
641
|
+
setIsMenuOpen(false);
|
|
642
|
+
signOut();
|
|
643
|
+
}}
|
|
644
|
+
>
|
|
645
|
+
Sign Out
|
|
646
|
+
</button>
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</li>
|
|
650
|
+
<li>
|
|
651
|
+
<Link
|
|
652
|
+
href="/dashboard"
|
|
653
|
+
className="block text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
654
|
+
onClick={() => setIsMenuOpen(false)}
|
|
655
|
+
>
|
|
656
|
+
Overview
|
|
657
|
+
</Link>
|
|
658
|
+
</li>
|
|
659
|
+
<li>
|
|
660
|
+
<Link
|
|
661
|
+
href="/dashboard/events"
|
|
662
|
+
className="block text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
663
|
+
onClick={() => setIsMenuOpen(false)}
|
|
664
|
+
>
|
|
665
|
+
Events
|
|
666
|
+
</Link>
|
|
667
|
+
</li>
|
|
668
|
+
<li>
|
|
669
|
+
<Link
|
|
670
|
+
href="/dashboard/explore"
|
|
671
|
+
className="block text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
672
|
+
onClick={() => setIsMenuOpen(false)}
|
|
673
|
+
>
|
|
674
|
+
Explore
|
|
675
|
+
</Link>
|
|
676
|
+
</li>
|
|
677
|
+
</ul>
|
|
678
|
+
</div>
|
|
679
|
+
)}
|
|
680
|
+
{isCreateTeamOpen && (
|
|
681
|
+
<div className="fixed inset-0 z-[90] flex items-center justify-center bg-black/50 px-4">
|
|
682
|
+
<div className="w-[min(32rem,calc(100vw-2rem))] rounded-lg border border-[var(--theme-border-primary)] bg-[var(--theme-bg-primary)] p-6 shadow-xl">
|
|
683
|
+
<div className="flex items-start justify-between gap-4">
|
|
684
|
+
<div className="max-w-[22rem]">
|
|
685
|
+
<h2 className="text-lg font-semibold text-[var(--theme-text-primary)]">
|
|
686
|
+
Create a new team
|
|
687
|
+
</h2>
|
|
688
|
+
<p className="mt-1 text-sm text-[var(--theme-text-secondary)]">
|
|
689
|
+
Teams are shared spaces for projects, sites, and reports.
|
|
690
|
+
</p>
|
|
691
|
+
</div>
|
|
692
|
+
<button
|
|
693
|
+
type="button"
|
|
694
|
+
className="rounded-full border border-[var(--theme-border-primary)] p-1 text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] transition-colors"
|
|
695
|
+
onClick={closeCreateTeamModal}
|
|
696
|
+
aria-label="Close"
|
|
697
|
+
>
|
|
698
|
+
<svg
|
|
699
|
+
width="16"
|
|
700
|
+
height="16"
|
|
701
|
+
viewBox="0 0 24 24"
|
|
702
|
+
fill="none"
|
|
703
|
+
stroke="currentColor"
|
|
704
|
+
strokeWidth="2"
|
|
705
|
+
strokeLinecap="round"
|
|
706
|
+
strokeLinejoin="round"
|
|
707
|
+
aria-hidden="true"
|
|
708
|
+
>
|
|
709
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
710
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
711
|
+
</svg>
|
|
712
|
+
</button>
|
|
713
|
+
</div>
|
|
714
|
+
<form onSubmit={handleCreateTeam} className="mt-5 space-y-4">
|
|
715
|
+
<div>
|
|
716
|
+
<label className="block text-sm font-medium text-[var(--theme-text-primary)]">
|
|
717
|
+
Team name
|
|
718
|
+
</label>
|
|
719
|
+
<input
|
|
720
|
+
value={createTeamName}
|
|
721
|
+
onChange={(event) => setCreateTeamName(event.target.value)}
|
|
722
|
+
className="mt-2 w-full rounded-md border border-[var(--theme-input-border)] bg-[var(--theme-input-bg)] px-3 py-2 text-[var(--theme-text-primary)] focus:border-[var(--theme-border-primary)] focus:outline-none"
|
|
723
|
+
placeholder="e.g. Lytx Analytics"
|
|
724
|
+
maxLength={80}
|
|
725
|
+
autoFocus
|
|
726
|
+
/>
|
|
727
|
+
</div>
|
|
728
|
+
{createTeamError ? (
|
|
729
|
+
<p className="text-sm text-red-500">{createTeamError}</p>
|
|
730
|
+
) : null}
|
|
731
|
+
<div className="flex items-center justify-end gap-2">
|
|
732
|
+
<button
|
|
733
|
+
type="button"
|
|
734
|
+
className="rounded-md border border-[var(--theme-border-primary)] px-4 py-2 text-sm text-[var(--theme-text-primary)] hover:text-[var(--color-primary)] transition-colors"
|
|
735
|
+
onClick={closeCreateTeamModal}
|
|
736
|
+
>
|
|
737
|
+
Cancel
|
|
738
|
+
</button>
|
|
739
|
+
<button
|
|
740
|
+
type="submit"
|
|
741
|
+
className="rounded-md bg-[var(--color-primary)] px-4 py-2 text-sm font-semibold text-white transition-opacity disabled:cursor-not-allowed disabled:opacity-70"
|
|
742
|
+
disabled={
|
|
743
|
+
isCreatingTeam || switchingTeamId !== null || !createTeamName.trim()
|
|
744
|
+
}
|
|
745
|
+
>
|
|
746
|
+
{isCreatingTeam ? "Creating..." : "Create team"}
|
|
747
|
+
</button>
|
|
748
|
+
</div>
|
|
749
|
+
</form>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</>
|
|
754
|
+
);
|
|
755
|
+
}
|