From 0024691da88a3681c17b27ac24426cbcdcca9620 Mon Sep 17 00:00:00 2001
From: Ben Senescu <44480372+bensenescu@users.noreply.github.com>
Date: Tue, 14 Apr 2026 20:26:58 -0400
Subject: [PATCH] Improve rank tracking empty state and add config route (#112)
---
.../rank-tracking/RankTrackingDomainList.tsx | 39 +++++--
.../rank-tracking/RankTrackingPage.tsx | 107 ------------------
src/routeTree.gen.ts | 69 ++++++++++-
.../_project/p/$projectId/rank-tracking.tsx | 23 +++-
.../p/$projectId/rank-tracking/$configId.tsx | 80 +++++++++++++
.../p/$projectId/rank-tracking/index.tsx | 45 ++++++++
6 files changed, 234 insertions(+), 129 deletions(-)
delete mode 100644 src/client/features/rank-tracking/RankTrackingPage.tsx
create mode 100644 src/routes/_project/p/$projectId/rank-tracking/$configId.tsx
create mode 100644 src/routes/_project/p/$projectId/rank-tracking/index.tsx
diff --git a/src/client/features/rank-tracking/RankTrackingDomainList.tsx b/src/client/features/rank-tracking/RankTrackingDomainList.tsx
index 6a98a56..a352a11 100644
--- a/src/client/features/rank-tracking/RankTrackingDomainList.tsx
+++ b/src/client/features/rank-tracking/RankTrackingDomainList.tsx
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
+import { useNavigate } from "@tanstack/react-router";
import { LOCATIONS } from "@/client/features/keywords/locations";
import { AlertTriangle, Globe, Plus, ChevronRight } from "lucide-react";
import { getRankTrackingConfigSummaries } from "@/serverFunctions/rank-tracking";
@@ -13,13 +14,12 @@ type ConfigSummary = Awaited<
export function RankTrackingDomainList({
projectId,
- onSelectConfig,
onAddDomain,
}: {
projectId: string;
- onSelectConfig: (configId: string) => void;
onAddDomain: () => void;
}) {
+ const navigate = useNavigate();
const { data: summaries } = useQuery({
queryKey: ["rankTrackingConfigSummaries", projectId],
queryFn: () => getRankTrackingConfigSummaries({ data: { projectId } }),
@@ -31,7 +31,7 @@ export function RankTrackingDomainList({
Tracked Domains
- {(summaries ?? []).map((summary) => (
-
onSelectConfig(summary.id)}
- />
- ))}
+ {(summaries ?? []).length === 0 ? (
+
+
+
+
+
+ No tracked domains yet
+
+
+ Add a domain to start monitoring keyword rankings over time.
+
+
+ ) : (
+ (summaries ?? []).map((summary) => (
+
+ void navigate({
+ to: "/p/$projectId/rank-tracking/$configId",
+ params: { projectId, configId: summary.id },
+ })
+ }
+ />
+ ))
+ )}
diff --git a/src/client/features/rank-tracking/RankTrackingPage.tsx b/src/client/features/rank-tracking/RankTrackingPage.tsx
deleted file mode 100644
index 108368d..0000000
--- a/src/client/features/rank-tracking/RankTrackingPage.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useState } from "react";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { Loader2, TrendingUp } from "lucide-react";
-import { getRankTrackingConfigs } from "@/serverFunctions/rank-tracking";
-import type { RankTrackingConfig } from "@/types/schemas/rank-tracking";
-import { RankTrackingDomainList } from "./RankTrackingDomainList";
-import { RankTrackingDomainDetail } from "./RankTrackingDomainDetail";
-import { RankTrackingConfigModal } from "./RankTrackingConfigModal";
-
-export function RankTrackingPage({ projectId }: { projectId: string }) {
- const queryClient = useQueryClient();
- const [selectedConfigId, setSelectedConfigId] = useState(null);
- const [showConfigModal, setShowConfigModal] = useState(false);
- const [editingConfig, setEditingConfig] = useState(
- null,
- );
-
- const { data: configs, isLoading } = useQuery({
- queryKey: ["rankTrackingConfigs", projectId],
- queryFn: () => getRankTrackingConfigs({ data: { projectId } }),
- });
-
- const invalidateConfigs = () => {
- void queryClient.invalidateQueries({
- queryKey: ["rankTrackingConfigs", projectId],
- });
- void queryClient.invalidateQueries({
- queryKey: ["rankTrackingConfigSummaries", projectId],
- });
- };
-
- const selectedConfig =
- configs?.find((c) => c.id === selectedConfigId) ?? null;
-
- const openAddModal = () => {
- setEditingConfig(null);
- setShowConfigModal(true);
- };
-
- const openEditModal = (config: RankTrackingConfig) => {
- setEditingConfig(config);
- setShowConfigModal(true);
- };
-
- return (
-
-
-
-
Rank Tracking
-
- Track keyword positions across domains
-
-
-
- {isLoading ? (
-
-
-
- ) : !configs || configs.length === 0 ? (
-
-
-
-
-
- Track your keyword positions over time
-
-
- Monitor rankings for your saved keywords, see position changes,
- and spot trends across desktop and mobile.
-
-
-
- Requires saved keywords in this project.
-
-
- ) : selectedConfig ? (
-
setSelectedConfigId(null)}
- onEdit={() => openEditModal(selectedConfig)}
- />
- ) : (
-
- )}
-
-
- {showConfigModal && (
-
setShowConfigModal(false)}
- onSaved={() => {
- setShowConfigModal(false);
- invalidateConfigs();
- }}
- />
- )}
-
- );
-}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 25c457f..c5aeade 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -34,7 +34,9 @@ import { Route as ProjectPProjectIdDomainRouteImport } from './routes/_project/p
import { Route as ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks'
import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit'
import { Route as ProjectPProjectIdAiRouteImport } from './routes/_project/p/$projectId/ai'
+import { Route as ProjectPProjectIdRankTrackingIndexRouteImport } from './routes/_project/p/$projectId/rank-tracking/index'
import { Route as ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index'
+import { Route as ProjectPProjectIdRankTrackingConfigIdRouteImport } from './routes/_project/p/$projectId/rank-tracking/$configId'
import { Route as ProjectPProjectIdAuditIssuesResultIdRouteImport } from './routes/_project/p/$projectId/audit/issues/$resultId'
const VerifyEmailRoute = VerifyEmailRouteImport.update({
@@ -161,12 +163,24 @@ const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({
path: '/ai',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
+const ProjectPProjectIdRankTrackingIndexRoute =
+ ProjectPProjectIdRankTrackingIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => ProjectPProjectIdRankTrackingRoute,
+ } as any)
const ProjectPProjectIdAuditIndexRoute =
ProjectPProjectIdAuditIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => ProjectPProjectIdAuditRoute,
} as any)
+const ProjectPProjectIdRankTrackingConfigIdRoute =
+ ProjectPProjectIdRankTrackingConfigIdRouteImport.update({
+ id: '/$configId',
+ path: '/$configId',
+ getParentRoute: () => ProjectPProjectIdRankTrackingRoute,
+ } as any)
const ProjectPProjectIdAuditIssuesResultIdRoute =
ProjectPProjectIdAuditIssuesResultIdRouteImport.update({
id: '/issues/$resultId',
@@ -193,10 +207,12 @@ export interface FileRoutesByFullPath {
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
- '/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRoute
+ '/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRouteWithChildren
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId/': typeof ProjectPProjectIdIndexRoute
+ '/p/$projectId/rank-tracking/$configId': typeof ProjectPProjectIdRankTrackingConfigIdRoute
'/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
+ '/p/$projectId/rank-tracking/': typeof ProjectPProjectIdRankTrackingIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
export interface FileRoutesByTo {
@@ -216,10 +232,11 @@ export interface FileRoutesByTo {
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
- '/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRoute
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId': typeof ProjectPProjectIdIndexRoute
+ '/p/$projectId/rank-tracking/$configId': typeof ProjectPProjectIdRankTrackingConfigIdRoute
'/p/$projectId/audit': typeof ProjectPProjectIdAuditIndexRoute
+ '/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
export interface FileRoutesById {
@@ -246,10 +263,12 @@ export interface FileRoutesById {
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/_project/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/_project/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
- '/_project/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRoute
+ '/_project/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRouteWithChildren
'/_project/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/_project/p/$projectId/': typeof ProjectPProjectIdIndexRoute
+ '/_project/p/$projectId/rank-tracking/$configId': typeof ProjectPProjectIdRankTrackingConfigIdRoute
'/_project/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
+ '/_project/p/$projectId/rank-tracking/': typeof ProjectPProjectIdRankTrackingIndexRoute
'/_project/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
export interface FileRouteTypes {
@@ -276,7 +295,9 @@ export interface FileRouteTypes {
| '/p/$projectId/rank-tracking'
| '/p/$projectId/saved'
| '/p/$projectId/'
+ | '/p/$projectId/rank-tracking/$configId'
| '/p/$projectId/audit/'
+ | '/p/$projectId/rank-tracking/'
| '/p/$projectId/audit/issues/$resultId'
fileRoutesByTo: FileRoutesByTo
to:
@@ -296,10 +317,11 @@ export interface FileRouteTypes {
| '/p/$projectId/backlinks'
| '/p/$projectId/domain'
| '/p/$projectId/keywords'
- | '/p/$projectId/rank-tracking'
| '/p/$projectId/saved'
| '/p/$projectId'
+ | '/p/$projectId/rank-tracking/$configId'
| '/p/$projectId/audit'
+ | '/p/$projectId/rank-tracking'
| '/p/$projectId/audit/issues/$resultId'
id:
| '__root__'
@@ -328,7 +350,9 @@ export interface FileRouteTypes {
| '/_project/p/$projectId/rank-tracking'
| '/_project/p/$projectId/saved'
| '/_project/p/$projectId/'
+ | '/_project/p/$projectId/rank-tracking/$configId'
| '/_project/p/$projectId/audit/'
+ | '/_project/p/$projectId/rank-tracking/'
| '/_project/p/$projectId/audit/issues/$resultId'
fileRoutesById: FileRoutesById
}
@@ -521,6 +545,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectPProjectIdAiRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
+ '/_project/p/$projectId/rank-tracking/': {
+ id: '/_project/p/$projectId/rank-tracking/'
+ path: '/'
+ fullPath: '/p/$projectId/rank-tracking/'
+ preLoaderRoute: typeof ProjectPProjectIdRankTrackingIndexRouteImport
+ parentRoute: typeof ProjectPProjectIdRankTrackingRoute
+ }
'/_project/p/$projectId/audit/': {
id: '/_project/p/$projectId/audit/'
path: '/'
@@ -528,6 +559,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectPProjectIdAuditIndexRouteImport
parentRoute: typeof ProjectPProjectIdAuditRoute
}
+ '/_project/p/$projectId/rank-tracking/$configId': {
+ id: '/_project/p/$projectId/rank-tracking/$configId'
+ path: '/$configId'
+ fullPath: '/p/$projectId/rank-tracking/$configId'
+ preLoaderRoute: typeof ProjectPProjectIdRankTrackingConfigIdRouteImport
+ parentRoute: typeof ProjectPProjectIdRankTrackingRoute
+ }
'/_project/p/$projectId/audit/issues/$resultId': {
id: '/_project/p/$projectId/audit/issues/$resultId'
path: '/issues/$resultId'
@@ -573,13 +611,31 @@ const ProjectPProjectIdAuditRouteWithChildren =
ProjectPProjectIdAuditRouteChildren,
)
+interface ProjectPProjectIdRankTrackingRouteChildren {
+ ProjectPProjectIdRankTrackingConfigIdRoute: typeof ProjectPProjectIdRankTrackingConfigIdRoute
+ ProjectPProjectIdRankTrackingIndexRoute: typeof ProjectPProjectIdRankTrackingIndexRoute
+}
+
+const ProjectPProjectIdRankTrackingRouteChildren: ProjectPProjectIdRankTrackingRouteChildren =
+ {
+ ProjectPProjectIdRankTrackingConfigIdRoute:
+ ProjectPProjectIdRankTrackingConfigIdRoute,
+ ProjectPProjectIdRankTrackingIndexRoute:
+ ProjectPProjectIdRankTrackingIndexRoute,
+ }
+
+const ProjectPProjectIdRankTrackingRouteWithChildren =
+ ProjectPProjectIdRankTrackingRoute._addFileChildren(
+ ProjectPProjectIdRankTrackingRouteChildren,
+ )
+
interface ProjectPProjectIdRouteRouteChildren {
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
ProjectPProjectIdDomainRoute: typeof ProjectPProjectIdDomainRoute
ProjectPProjectIdKeywordsRoute: typeof ProjectPProjectIdKeywordsRoute
- ProjectPProjectIdRankTrackingRoute: typeof ProjectPProjectIdRankTrackingRoute
+ ProjectPProjectIdRankTrackingRoute: typeof ProjectPProjectIdRankTrackingRouteWithChildren
ProjectPProjectIdSavedRoute: typeof ProjectPProjectIdSavedRoute
ProjectPProjectIdIndexRoute: typeof ProjectPProjectIdIndexRoute
}
@@ -591,7 +647,8 @@ const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren =
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
ProjectPProjectIdDomainRoute: ProjectPProjectIdDomainRoute,
ProjectPProjectIdKeywordsRoute: ProjectPProjectIdKeywordsRoute,
- ProjectPProjectIdRankTrackingRoute: ProjectPProjectIdRankTrackingRoute,
+ ProjectPProjectIdRankTrackingRoute:
+ ProjectPProjectIdRankTrackingRouteWithChildren,
ProjectPProjectIdSavedRoute: ProjectPProjectIdSavedRoute,
ProjectPProjectIdIndexRoute: ProjectPProjectIdIndexRoute,
}
diff --git a/src/routes/_project/p/$projectId/rank-tracking.tsx b/src/routes/_project/p/$projectId/rank-tracking.tsx
index 17c65f5..ea32014 100644
--- a/src/routes/_project/p/$projectId/rank-tracking.tsx
+++ b/src/routes/_project/p/$projectId/rank-tracking.tsx
@@ -1,11 +1,22 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { RankTrackingPage } from "@/client/features/rank-tracking/RankTrackingPage";
+import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/_project/p/$projectId/rank-tracking")({
- component: RankTrackingRoute,
+ component: RankTrackingLayout,
});
-function RankTrackingRoute() {
- const { projectId } = Route.useParams();
- return ;
+function RankTrackingLayout() {
+ return (
+
+
+
+
Rank Tracking
+
+ Track keyword positions across domains
+
+
+
+
+
+
+ );
}
diff --git a/src/routes/_project/p/$projectId/rank-tracking/$configId.tsx b/src/routes/_project/p/$projectId/rank-tracking/$configId.tsx
new file mode 100644
index 0000000..a93f029
--- /dev/null
+++ b/src/routes/_project/p/$projectId/rank-tracking/$configId.tsx
@@ -0,0 +1,80 @@
+import { useState } from "react";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { getRankTrackingConfigs } from "@/serverFunctions/rank-tracking";
+import { RankTrackingDomainDetail } from "@/client/features/rank-tracking/RankTrackingDomainDetail";
+import { RankTrackingConfigModal } from "@/client/features/rank-tracking/RankTrackingConfigModal";
+
+export const Route = createFileRoute(
+ "/_project/p/$projectId/rank-tracking/$configId",
+)({
+ component: RankTrackingConfigRoute,
+});
+
+function RankTrackingConfigRoute() {
+ const { projectId, configId } = Route.useParams();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const [showConfigModal, setShowConfigModal] = useState(false);
+
+ const { data: configs, isLoading } = useQuery({
+ queryKey: ["rankTrackingConfigs", projectId],
+ queryFn: () => getRankTrackingConfigs({ data: { projectId } }),
+ });
+
+ const config = configs?.find((c) => c.id === configId) ?? null;
+
+ const invalidateConfigs = () => {
+ void queryClient.invalidateQueries({
+ queryKey: ["rankTrackingConfigs", projectId],
+ });
+ void queryClient.invalidateQueries({
+ queryKey: ["rankTrackingConfigSummaries", projectId],
+ });
+ };
+
+ const handleBack = () => {
+ void navigate({
+ to: "/p/$projectId/rank-tracking",
+ params: { projectId },
+ });
+ };
+
+ if (isLoading) return null;
+
+ if (!config) {
+ return (
+ <>
+
+ Domain configuration not found.
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+ setShowConfigModal(true)}
+ />
+
+ {showConfigModal && (
+ setShowConfigModal(false)}
+ onSaved={() => {
+ setShowConfigModal(false);
+ invalidateConfigs();
+ }}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/routes/_project/p/$projectId/rank-tracking/index.tsx b/src/routes/_project/p/$projectId/rank-tracking/index.tsx
new file mode 100644
index 0000000..46d105b
--- /dev/null
+++ b/src/routes/_project/p/$projectId/rank-tracking/index.tsx
@@ -0,0 +1,45 @@
+import { useState } from "react";
+import { createFileRoute } from "@tanstack/react-router";
+import { useQueryClient } from "@tanstack/react-query";
+import { RankTrackingDomainList } from "@/client/features/rank-tracking/RankTrackingDomainList";
+import { RankTrackingConfigModal } from "@/client/features/rank-tracking/RankTrackingConfigModal";
+
+export const Route = createFileRoute("/_project/p/$projectId/rank-tracking/")({
+ component: RankTrackingIndex,
+});
+
+function RankTrackingIndex() {
+ const { projectId } = Route.useParams();
+ const queryClient = useQueryClient();
+ const [showConfigModal, setShowConfigModal] = useState(false);
+
+ const invalidateConfigs = () => {
+ void queryClient.invalidateQueries({
+ queryKey: ["rankTrackingConfigs", projectId],
+ });
+ void queryClient.invalidateQueries({
+ queryKey: ["rankTrackingConfigSummaries", projectId],
+ });
+ };
+
+ return (
+ <>
+ setShowConfigModal(true)}
+ />
+
+ {showConfigModal && (
+ setShowConfigModal(false)}
+ onSaved={() => {
+ setShowConfigModal(false);
+ invalidateConfigs();
+ }}
+ />
+ )}
+ >
+ );
+}