Improve rank tracking empty state and add config route (#112)
This commit is contained in:
parent
e68ced7203
commit
0024691da8
@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { LOCATIONS } from "@/client/features/keywords/locations";
|
import { LOCATIONS } from "@/client/features/keywords/locations";
|
||||||
import { AlertTriangle, Globe, Plus, ChevronRight } from "lucide-react";
|
import { AlertTriangle, Globe, Plus, ChevronRight } from "lucide-react";
|
||||||
import { getRankTrackingConfigSummaries } from "@/serverFunctions/rank-tracking";
|
import { getRankTrackingConfigSummaries } from "@/serverFunctions/rank-tracking";
|
||||||
@ -13,13 +14,12 @@ type ConfigSummary = Awaited<
|
|||||||
|
|
||||||
export function RankTrackingDomainList({
|
export function RankTrackingDomainList({
|
||||||
projectId,
|
projectId,
|
||||||
onSelectConfig,
|
|
||||||
onAddDomain,
|
onAddDomain,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
onSelectConfig: (configId: string) => void;
|
|
||||||
onAddDomain: () => void;
|
onAddDomain: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { data: summaries } = useQuery({
|
const { data: summaries } = useQuery({
|
||||||
queryKey: ["rankTrackingConfigSummaries", projectId],
|
queryKey: ["rankTrackingConfigSummaries", projectId],
|
||||||
queryFn: () => getRankTrackingConfigSummaries({ data: { projectId } }),
|
queryFn: () => getRankTrackingConfigSummaries({ data: { projectId } }),
|
||||||
@ -31,7 +31,7 @@ export function RankTrackingDomainList({
|
|||||||
<div className="flex items-center justify-between px-5 pt-4 pb-3">
|
<div className="flex items-center justify-between px-5 pt-4 pb-3">
|
||||||
<h2 className="text-sm font-semibold">Tracked Domains</h2>
|
<h2 className="text-sm font-semibold">Tracked Domains</h2>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-sm btn-outline gap-1"
|
className="btn btn-primary btn-sm gap-1"
|
||||||
onClick={onAddDomain}
|
onClick={onAddDomain}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
@ -39,13 +39,32 @@ export function RankTrackingDomainList({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-base-300">
|
<div className="divide-y divide-base-300">
|
||||||
{(summaries ?? []).map((summary) => (
|
{(summaries ?? []).length === 0 ? (
|
||||||
|
<div className="px-5 py-10 text-center space-y-2">
|
||||||
|
<div className="mx-auto flex size-10 items-center justify-center rounded-xl bg-base-200">
|
||||||
|
<Globe className="size-5 text-base-content/40" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-base-content/70">
|
||||||
|
No tracked domains yet
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-base-content/40">
|
||||||
|
Add a domain to start monitoring keyword rankings over time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(summaries ?? []).map((summary) => (
|
||||||
<DomainRow
|
<DomainRow
|
||||||
key={summary.id}
|
key={summary.id}
|
||||||
summary={summary}
|
summary={summary}
|
||||||
onClick={() => onSelectConfig(summary.id)}
|
onClick={() =>
|
||||||
|
void navigate({
|
||||||
|
to: "/p/$projectId/rank-tracking/$configId",
|
||||||
|
params: { projectId, configId: summary.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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<string | null>(null);
|
|
||||||
const [showConfigModal, setShowConfigModal] = useState(false);
|
|
||||||
const [editingConfig, setEditingConfig] = useState<RankTrackingConfig | null>(
|
|
||||||
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 (
|
|
||||||
<div className="px-4 py-4 pb-24 overflow-auto md:px-6 md:py-6 md:pb-8">
|
|
||||||
<div className="mx-auto max-w-7xl space-y-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-semibold">Rank Tracking</h1>
|
|
||||||
<p className="text-sm text-base-content/70">
|
|
||||||
Track keyword positions across domains
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center p-12">
|
|
||||||
<Loader2 className="size-6 animate-spin text-base-content/50" />
|
|
||||||
</div>
|
|
||||||
) : !configs || configs.length === 0 ? (
|
|
||||||
<section className="rounded-2xl border border-dashed border-base-300 bg-base-100/70 p-8 text-center space-y-3">
|
|
||||||
<div className="mx-auto flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
||||||
<TrendingUp className="size-6" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-lg font-medium text-base-content/80">
|
|
||||||
Track your keyword positions over time
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-base-content/55 max-w-md mx-auto">
|
|
||||||
Monitor rankings for your saved keywords, see position changes,
|
|
||||||
and spot trends across desktop and mobile.
|
|
||||||
</p>
|
|
||||||
<button className="btn btn-primary btn-sm" onClick={openAddModal}>
|
|
||||||
Add Domain
|
|
||||||
</button>
|
|
||||||
<p className="text-xs text-base-content/50">
|
|
||||||
Requires saved keywords in this project.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
) : selectedConfig ? (
|
|
||||||
<RankTrackingDomainDetail
|
|
||||||
config={selectedConfig}
|
|
||||||
projectId={projectId}
|
|
||||||
onBack={() => setSelectedConfigId(null)}
|
|
||||||
onEdit={() => openEditModal(selectedConfig)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<RankTrackingDomainList
|
|
||||||
projectId={projectId}
|
|
||||||
onSelectConfig={setSelectedConfigId}
|
|
||||||
onAddDomain={openAddModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showConfigModal && (
|
|
||||||
<RankTrackingConfigModal
|
|
||||||
projectId={projectId}
|
|
||||||
existingConfig={editingConfig}
|
|
||||||
onClose={() => setShowConfigModal(false)}
|
|
||||||
onSaved={() => {
|
|
||||||
setShowConfigModal(false);
|
|
||||||
invalidateConfigs();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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 ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks'
|
||||||
import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit'
|
import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit'
|
||||||
import { Route as ProjectPProjectIdAiRouteImport } from './routes/_project/p/$projectId/ai'
|
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 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'
|
import { Route as ProjectPProjectIdAuditIssuesResultIdRouteImport } from './routes/_project/p/$projectId/audit/issues/$resultId'
|
||||||
|
|
||||||
const VerifyEmailRoute = VerifyEmailRouteImport.update({
|
const VerifyEmailRoute = VerifyEmailRouteImport.update({
|
||||||
@ -161,12 +163,24 @@ const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({
|
|||||||
path: '/ai',
|
path: '/ai',
|
||||||
getParentRoute: () => ProjectPProjectIdRouteRoute,
|
getParentRoute: () => ProjectPProjectIdRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ProjectPProjectIdRankTrackingIndexRoute =
|
||||||
|
ProjectPProjectIdRankTrackingIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => ProjectPProjectIdRankTrackingRoute,
|
||||||
|
} as any)
|
||||||
const ProjectPProjectIdAuditIndexRoute =
|
const ProjectPProjectIdAuditIndexRoute =
|
||||||
ProjectPProjectIdAuditIndexRouteImport.update({
|
ProjectPProjectIdAuditIndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => ProjectPProjectIdAuditRoute,
|
getParentRoute: () => ProjectPProjectIdAuditRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ProjectPProjectIdRankTrackingConfigIdRoute =
|
||||||
|
ProjectPProjectIdRankTrackingConfigIdRouteImport.update({
|
||||||
|
id: '/$configId',
|
||||||
|
path: '/$configId',
|
||||||
|
getParentRoute: () => ProjectPProjectIdRankTrackingRoute,
|
||||||
|
} as any)
|
||||||
const ProjectPProjectIdAuditIssuesResultIdRoute =
|
const ProjectPProjectIdAuditIssuesResultIdRoute =
|
||||||
ProjectPProjectIdAuditIssuesResultIdRouteImport.update({
|
ProjectPProjectIdAuditIssuesResultIdRouteImport.update({
|
||||||
id: '/issues/$resultId',
|
id: '/issues/$resultId',
|
||||||
@ -193,10 +207,12 @@ export interface FileRoutesByFullPath {
|
|||||||
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
||||||
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
||||||
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
||||||
'/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRoute
|
'/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRouteWithChildren
|
||||||
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
||||||
'/p/$projectId/': typeof ProjectPProjectIdIndexRoute
|
'/p/$projectId/': typeof ProjectPProjectIdIndexRoute
|
||||||
|
'/p/$projectId/rank-tracking/$configId': typeof ProjectPProjectIdRankTrackingConfigIdRoute
|
||||||
'/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
|
'/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
|
||||||
|
'/p/$projectId/rank-tracking/': typeof ProjectPProjectIdRankTrackingIndexRoute
|
||||||
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@ -216,10 +232,11 @@ export interface FileRoutesByTo {
|
|||||||
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
||||||
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
||||||
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
||||||
'/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingRoute
|
|
||||||
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
||||||
'/p/$projectId': typeof ProjectPProjectIdIndexRoute
|
'/p/$projectId': typeof ProjectPProjectIdIndexRoute
|
||||||
|
'/p/$projectId/rank-tracking/$configId': typeof ProjectPProjectIdRankTrackingConfigIdRoute
|
||||||
'/p/$projectId/audit': typeof ProjectPProjectIdAuditIndexRoute
|
'/p/$projectId/audit': typeof ProjectPProjectIdAuditIndexRoute
|
||||||
|
'/p/$projectId/rank-tracking': typeof ProjectPProjectIdRankTrackingIndexRoute
|
||||||
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@ -246,10 +263,12 @@ export interface FileRoutesById {
|
|||||||
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
||||||
'/_project/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
'/_project/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
||||||
'/_project/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
'/_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/saved': typeof ProjectPProjectIdSavedRoute
|
||||||
'/_project/p/$projectId/': typeof ProjectPProjectIdIndexRoute
|
'/_project/p/$projectId/': typeof ProjectPProjectIdIndexRoute
|
||||||
|
'/_project/p/$projectId/rank-tracking/$configId': typeof ProjectPProjectIdRankTrackingConfigIdRoute
|
||||||
'/_project/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
|
'/_project/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
|
||||||
|
'/_project/p/$projectId/rank-tracking/': typeof ProjectPProjectIdRankTrackingIndexRoute
|
||||||
'/_project/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
'/_project/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@ -276,7 +295,9 @@ export interface FileRouteTypes {
|
|||||||
| '/p/$projectId/rank-tracking'
|
| '/p/$projectId/rank-tracking'
|
||||||
| '/p/$projectId/saved'
|
| '/p/$projectId/saved'
|
||||||
| '/p/$projectId/'
|
| '/p/$projectId/'
|
||||||
|
| '/p/$projectId/rank-tracking/$configId'
|
||||||
| '/p/$projectId/audit/'
|
| '/p/$projectId/audit/'
|
||||||
|
| '/p/$projectId/rank-tracking/'
|
||||||
| '/p/$projectId/audit/issues/$resultId'
|
| '/p/$projectId/audit/issues/$resultId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
@ -296,10 +317,11 @@ export interface FileRouteTypes {
|
|||||||
| '/p/$projectId/backlinks'
|
| '/p/$projectId/backlinks'
|
||||||
| '/p/$projectId/domain'
|
| '/p/$projectId/domain'
|
||||||
| '/p/$projectId/keywords'
|
| '/p/$projectId/keywords'
|
||||||
| '/p/$projectId/rank-tracking'
|
|
||||||
| '/p/$projectId/saved'
|
| '/p/$projectId/saved'
|
||||||
| '/p/$projectId'
|
| '/p/$projectId'
|
||||||
|
| '/p/$projectId/rank-tracking/$configId'
|
||||||
| '/p/$projectId/audit'
|
| '/p/$projectId/audit'
|
||||||
|
| '/p/$projectId/rank-tracking'
|
||||||
| '/p/$projectId/audit/issues/$resultId'
|
| '/p/$projectId/audit/issues/$resultId'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
@ -328,7 +350,9 @@ export interface FileRouteTypes {
|
|||||||
| '/_project/p/$projectId/rank-tracking'
|
| '/_project/p/$projectId/rank-tracking'
|
||||||
| '/_project/p/$projectId/saved'
|
| '/_project/p/$projectId/saved'
|
||||||
| '/_project/p/$projectId/'
|
| '/_project/p/$projectId/'
|
||||||
|
| '/_project/p/$projectId/rank-tracking/$configId'
|
||||||
| '/_project/p/$projectId/audit/'
|
| '/_project/p/$projectId/audit/'
|
||||||
|
| '/_project/p/$projectId/rank-tracking/'
|
||||||
| '/_project/p/$projectId/audit/issues/$resultId'
|
| '/_project/p/$projectId/audit/issues/$resultId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
@ -521,6 +545,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProjectPProjectIdAiRouteImport
|
preLoaderRoute: typeof ProjectPProjectIdAiRouteImport
|
||||||
parentRoute: typeof ProjectPProjectIdRouteRoute
|
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/': {
|
'/_project/p/$projectId/audit/': {
|
||||||
id: '/_project/p/$projectId/audit/'
|
id: '/_project/p/$projectId/audit/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@ -528,6 +559,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProjectPProjectIdAuditIndexRouteImport
|
preLoaderRoute: typeof ProjectPProjectIdAuditIndexRouteImport
|
||||||
parentRoute: typeof ProjectPProjectIdAuditRoute
|
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': {
|
'/_project/p/$projectId/audit/issues/$resultId': {
|
||||||
id: '/_project/p/$projectId/audit/issues/$resultId'
|
id: '/_project/p/$projectId/audit/issues/$resultId'
|
||||||
path: '/issues/$resultId'
|
path: '/issues/$resultId'
|
||||||
@ -573,13 +611,31 @@ const ProjectPProjectIdAuditRouteWithChildren =
|
|||||||
ProjectPProjectIdAuditRouteChildren,
|
ProjectPProjectIdAuditRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface ProjectPProjectIdRankTrackingRouteChildren {
|
||||||
|
ProjectPProjectIdRankTrackingConfigIdRoute: typeof ProjectPProjectIdRankTrackingConfigIdRoute
|
||||||
|
ProjectPProjectIdRankTrackingIndexRoute: typeof ProjectPProjectIdRankTrackingIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectPProjectIdRankTrackingRouteChildren: ProjectPProjectIdRankTrackingRouteChildren =
|
||||||
|
{
|
||||||
|
ProjectPProjectIdRankTrackingConfigIdRoute:
|
||||||
|
ProjectPProjectIdRankTrackingConfigIdRoute,
|
||||||
|
ProjectPProjectIdRankTrackingIndexRoute:
|
||||||
|
ProjectPProjectIdRankTrackingIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectPProjectIdRankTrackingRouteWithChildren =
|
||||||
|
ProjectPProjectIdRankTrackingRoute._addFileChildren(
|
||||||
|
ProjectPProjectIdRankTrackingRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
interface ProjectPProjectIdRouteRouteChildren {
|
interface ProjectPProjectIdRouteRouteChildren {
|
||||||
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
|
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
|
||||||
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
|
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
|
||||||
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
|
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
|
||||||
ProjectPProjectIdDomainRoute: typeof ProjectPProjectIdDomainRoute
|
ProjectPProjectIdDomainRoute: typeof ProjectPProjectIdDomainRoute
|
||||||
ProjectPProjectIdKeywordsRoute: typeof ProjectPProjectIdKeywordsRoute
|
ProjectPProjectIdKeywordsRoute: typeof ProjectPProjectIdKeywordsRoute
|
||||||
ProjectPProjectIdRankTrackingRoute: typeof ProjectPProjectIdRankTrackingRoute
|
ProjectPProjectIdRankTrackingRoute: typeof ProjectPProjectIdRankTrackingRouteWithChildren
|
||||||
ProjectPProjectIdSavedRoute: typeof ProjectPProjectIdSavedRoute
|
ProjectPProjectIdSavedRoute: typeof ProjectPProjectIdSavedRoute
|
||||||
ProjectPProjectIdIndexRoute: typeof ProjectPProjectIdIndexRoute
|
ProjectPProjectIdIndexRoute: typeof ProjectPProjectIdIndexRoute
|
||||||
}
|
}
|
||||||
@ -591,7 +647,8 @@ const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren =
|
|||||||
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
|
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
|
||||||
ProjectPProjectIdDomainRoute: ProjectPProjectIdDomainRoute,
|
ProjectPProjectIdDomainRoute: ProjectPProjectIdDomainRoute,
|
||||||
ProjectPProjectIdKeywordsRoute: ProjectPProjectIdKeywordsRoute,
|
ProjectPProjectIdKeywordsRoute: ProjectPProjectIdKeywordsRoute,
|
||||||
ProjectPProjectIdRankTrackingRoute: ProjectPProjectIdRankTrackingRoute,
|
ProjectPProjectIdRankTrackingRoute:
|
||||||
|
ProjectPProjectIdRankTrackingRouteWithChildren,
|
||||||
ProjectPProjectIdSavedRoute: ProjectPProjectIdSavedRoute,
|
ProjectPProjectIdSavedRoute: ProjectPProjectIdSavedRoute,
|
||||||
ProjectPProjectIdIndexRoute: ProjectPProjectIdIndexRoute,
|
ProjectPProjectIdIndexRoute: ProjectPProjectIdIndexRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,22 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||||
import { RankTrackingPage } from "@/client/features/rank-tracking/RankTrackingPage";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_project/p/$projectId/rank-tracking")({
|
export const Route = createFileRoute("/_project/p/$projectId/rank-tracking")({
|
||||||
component: RankTrackingRoute,
|
component: RankTrackingLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RankTrackingRoute() {
|
function RankTrackingLayout() {
|
||||||
const { projectId } = Route.useParams();
|
return (
|
||||||
return <RankTrackingPage projectId={projectId} />;
|
<div className="px-4 py-4 pb-24 overflow-auto md:px-6 md:py-6 md:pb-8">
|
||||||
|
<div className="mx-auto max-w-7xl space-y-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold">Rank Tracking</h1>
|
||||||
|
<p className="text-sm text-base-content/70">
|
||||||
|
Track keyword positions across domains
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/routes/_project/p/$projectId/rank-tracking/$configId.tsx
Normal file
80
src/routes/_project/p/$projectId/rank-tracking/$configId.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-base-content/70">
|
||||||
|
Domain configuration not found.
|
||||||
|
</p>
|
||||||
|
<button className="btn btn-ghost btn-sm" onClick={handleBack}>
|
||||||
|
Back to domains
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RankTrackingDomainDetail
|
||||||
|
config={config}
|
||||||
|
projectId={projectId}
|
||||||
|
onBack={handleBack}
|
||||||
|
onEdit={() => setShowConfigModal(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showConfigModal && (
|
||||||
|
<RankTrackingConfigModal
|
||||||
|
projectId={projectId}
|
||||||
|
existingConfig={config}
|
||||||
|
onClose={() => setShowConfigModal(false)}
|
||||||
|
onSaved={() => {
|
||||||
|
setShowConfigModal(false);
|
||||||
|
invalidateConfigs();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/routes/_project/p/$projectId/rank-tracking/index.tsx
Normal file
45
src/routes/_project/p/$projectId/rank-tracking/index.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<RankTrackingDomainList
|
||||||
|
projectId={projectId}
|
||||||
|
onAddDomain={() => setShowConfigModal(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showConfigModal && (
|
||||||
|
<RankTrackingConfigModal
|
||||||
|
projectId={projectId}
|
||||||
|
existingConfig={null}
|
||||||
|
onClose={() => setShowConfigModal(false)}
|
||||||
|
onSaved={() => {
|
||||||
|
setShowConfigModal(false);
|
||||||
|
invalidateConfigs();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user