refactor project layouts around route boundaries (#45)

* refactor: align app layouts with route boundaries

* fix: honor auth redirect search in auth layout
This commit is contained in:
Ben Senescu 2026-03-24 17:50:35 -04:00 committed by GitHub
parent 4040a854a7
commit bfc6a97466
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 748 additions and 676 deletions

View File

@ -3,42 +3,12 @@ import { ChevronsUpDown, X } from "lucide-react";
import { getProjectNavItems } from "@/client/navigation/items"; import { getProjectNavItems } from "@/client/navigation/items";
interface SidebarProps { interface SidebarProps {
currentPath: string; projectId: string;
projectId: string | null;
onNavigate?: () => void; onNavigate?: () => void;
onClose?: () => void; onClose?: () => void;
} }
export function Sidebar({ export function Sidebar({ projectId, onNavigate, onClose }: SidebarProps) {
currentPath,
projectId,
onNavigate,
onClose,
}: SidebarProps) {
// If we don't have a projectId yet (e.g., root redirect hasn't fired),
// don't render nav links since we can't build the URLs.
if (!projectId) {
return (
<div className="sidebar w-64 border-r border-base-300 h-full bg-base-100 flex flex-col">
<div className="px-4 py-4 border-b border-base-300 flex items-center justify-between">
<span className="font-semibold text-base-content">OpenSEO</span>
{onClose && (
<button
onClick={onClose}
className="btn btn-ghost btn-sm btn-circle"
aria-label="Close sidebar"
>
<X className="h-5 w-5" />
</button>
)}
</div>
<div className="flex-1 flex items-center justify-center">
<span className="loading loading-spinner loading-sm" />
</div>
</div>
);
}
const projectNavItems = getProjectNavItems(projectId); const projectNavItems = getProjectNavItems(projectId);
return ( return (
@ -73,25 +43,26 @@ export function Sidebar({
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 py-4 pl-3 overflow-y-auto"> <nav className="flex-1 py-4 pl-3 overflow-y-auto">
{projectNavItems.map((item) => { {projectNavItems.map((item) => {
const { icon: Icon, matchSegment, ...linkProps } = item; const { icon: Icon, ...linkProps } = item;
const isActive = currentPath.includes(matchSegment);
return ( return (
<Link <Link
key={linkProps.to} key={linkProps.to}
{...linkProps} {...linkProps}
onClick={onNavigate} onClick={onNavigate}
className={`relative flex items-center gap-3 pl-4 pr-4 py-2 text-sm transition-colors ${ activeOptions={{ exact: false, includeSearch: false }}
isActive className="relative flex items-center gap-3 px-4 py-2 text-sm text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content"
? "text-base-content font-medium" activeProps={{ className: "text-base-content font-medium" }}
: "text-base-content/60 hover:text-base-content hover:bg-base-200"
}`}
> >
{isActive && ( {({ isActive }: { isActive: boolean }) => (
<div className="absolute left-0 top-1 bottom-1 w-[3px] bg-primary rounded-r-full" /> <>
)} {isActive ? (
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-r-full bg-primary" />
) : null}
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
{item.label} {item.label}
</>
)}
</Link> </Link>
); );
})} })}

View File

@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { getCurrentAuthRedirect } from "@/lib/auth-redirect"; import { getSignInHref, getSignInHrefForLocation } from "@/lib/auth-redirect";
import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { isHostedClientAuthMode } from "@/lib/auth-mode";
type UnauthenticatedErrorCardProps = { type UnauthenticatedErrorCardProps = {
@ -7,22 +7,15 @@ type UnauthenticatedErrorCardProps = {
onRetry?: () => void; onRetry?: () => void;
}; };
function getSignInHref(redirectTo: string) {
return redirectTo === "/"
? "/sign-in"
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`;
}
export function UnauthenticatedErrorCard({ export function UnauthenticatedErrorCard({
message, message,
onRetry, onRetry,
}: UnauthenticatedErrorCardProps) { }: UnauthenticatedErrorCardProps) {
const redirectTo =
typeof window === "undefined"
? "/"
: getCurrentAuthRedirect(window.location);
const isHostedMode = isHostedClientAuthMode(); const isHostedMode = isHostedClientAuthMode();
const signInHref = getSignInHref(redirectTo); const signInHref =
typeof window === "undefined"
? getSignInHref("/")
: getSignInHrefForLocation(window.location);
useEffect(() => { useEffect(() => {
if (typeof window === "undefined" || !isHostedMode) { if (typeof window === "undefined" || !isHostedMode) {

View File

@ -1,5 +1,3 @@
import { useEffect } from "react";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { normalizeAuthRedirect } from "@/lib/auth-redirect"; import { normalizeAuthRedirect } from "@/lib/auth-redirect";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
@ -10,19 +8,10 @@ export const authRedirectSearchSchema = z.object({
}); });
export function useAuthPageState(redirect: string | undefined) { export function useAuthPageState(redirect: string | undefined) {
const navigate = useNavigate();
const redirectTo = normalizeAuthRedirect(redirect); const redirectTo = normalizeAuthRedirect(redirect);
const { data: session, isPending: isSessionPending } = useSession(); const { isPending: isSessionPending } = useSession();
const isHostedMode = isHostedClientAuthMode(); const isHostedMode = isHostedClientAuthMode();
useEffect(() => {
if (!session?.user?.id) {
return;
}
void navigate({ href: redirectTo });
}, [navigate, redirectTo, session?.user?.id]);
return { return {
redirectTo, redirectTo,
isHostedMode, isHostedMode,
@ -30,10 +19,6 @@ export function useAuthPageState(redirect: string | undefined) {
}; };
} }
export function getAuthLinkSearch(redirectTo: string) {
return redirectTo === "/" ? {} : { redirect: redirectTo };
}
export function getFieldError(errors: unknown[]) { export function getFieldError(errors: unknown[]) {
return typeof errors[0] === "string" ? errors[0] : null; return typeof errors[0] === "string" ? errors[0] : null;
} }

View File

@ -1,30 +1,132 @@
import * as React from "react"; import * as React from "react";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, useLocation } from "@tanstack/react-router";
import { ChevronsUpDown, Menu, User } from "lucide-react";
import { getProjectNavItems } from "@/client/navigation/items";
import { import {
AlertTriangle, AppContent,
ChevronsUpDown, MissingSeoSetupModal,
ExternalLink, SeoApiStatusBanners,
Menu, } from "@/client/layout/AppShellParts";
User, import { getSignInHrefForLocation } from "@/lib/auth-redirect";
} from "lucide-react";
import { Sidebar } from "@/client/components/Sidebar";
import {
dataforseoHelpLinkOptions,
getProjectNavItems,
} from "@/client/navigation/items";
import { getCurrentAuthRedirect } from "@/lib/auth-redirect";
import { authClient, useSession } from "@/lib/auth-client"; import { authClient, useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { isHostedClientAuthMode } from "@/lib/auth-mode";
import { getSeoApiKeyStatus } from "@/serverFunctions/config";
export function TopNav({ const DATAFORSEO_HELP_PATH = "/help/dataforseo-api-key";
export function AuthenticatedAppLayout({
children,
projectId,
}: {
children: React.ReactNode;
projectId?: string;
}) {
const location = useLocation();
const [drawerOpen, setDrawerOpen] = React.useState(false);
const setupModalRef = React.useRef<HTMLDivElement | null>(null);
const [isSeoApiKeyConfigured, setIsSeoApiKeyConfigured] = React.useState<
boolean | null
>(null);
const [seoApiKeyStatusError, setSeoApiKeyStatusError] = React.useState(false);
const [showMissingSeoApiKeyModal, setShowMissingSeoApiKeyModal] =
React.useState(false);
React.useEffect(() => {
let cancelled = false;
const checkSeoApiKeyStatus = async () => {
try {
const result = await getSeoApiKeyStatus();
if (cancelled) return;
setSeoApiKeyStatusError(false);
setIsSeoApiKeyConfigured(result.configured);
if (!result.configured) {
setShowMissingSeoApiKeyModal(true);
}
} catch {
if (cancelled) return;
setSeoApiKeyStatusError(true);
setIsSeoApiKeyConfigured(null);
setShowMissingSeoApiKeyModal(false);
}
};
void checkSeoApiKeyStatus();
return () => {
cancelled = true;
};
}, []);
const shouldShowMissingSeoApiKeyModal =
showMissingSeoApiKeyModal && location.pathname !== DATAFORSEO_HELP_PATH;
const shouldShowSeoApiWarning =
!seoApiKeyStatusError &&
isSeoApiKeyConfigured === false &&
!shouldShowMissingSeoApiKeyModal;
React.useEffect(() => {
if (!shouldShowMissingSeoApiKeyModal) return;
setupModalRef.current?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setShowMissingSeoApiKeyModal(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [shouldShowMissingSeoApiKeyModal]);
React.useEffect(() => {
if (!projectId) {
setDrawerOpen(false);
}
}, [projectId]);
return (
<div className="flex h-[100dvh] flex-col bg-base-200">
<TopNav
drawerOpen={drawerOpen}
projectId={projectId ?? null}
onOpenDrawer={() => setDrawerOpen(true)}
/>
<SeoApiStatusBanners
shouldShowSeoApiWarning={shouldShowSeoApiWarning}
seoApiKeyStatusError={seoApiKeyStatusError}
/>
<AppContent
drawerOpen={drawerOpen}
projectId={projectId ?? null}
onCloseDrawer={() => setDrawerOpen(false)}
>
{children}
</AppContent>
<MissingSeoSetupModal
ref={setupModalRef}
isOpen={shouldShowMissingSeoApiKeyModal}
onClose={() => setShowMissingSeoApiKeyModal(false)}
/>
</div>
);
}
function TopNav({
drawerOpen, drawerOpen,
projectId, projectId,
pathname,
onOpenDrawer, onOpenDrawer,
}: { }: {
drawerOpen: boolean; drawerOpen: boolean;
projectId: string | null; projectId: string | null;
pathname: string;
onOpenDrawer: () => void; onOpenDrawer: () => void;
}) { }) {
const isHostedMode = isHostedClientAuthMode(); const isHostedMode = isHostedClientAuthMode();
@ -33,6 +135,7 @@ export function TopNav({
return ( return (
<div className="navbar bg-base-100 border-b border-base-300 shrink-0 gap-2"> <div className="navbar bg-base-100 border-b border-base-300 shrink-0 gap-2">
<div className="flex-none flex items-center md:hidden"> <div className="flex-none flex items-center md:hidden">
{projectId ? (
<button <button
type="button" type="button"
className="btn btn-square btn-ghost" className="btn btn-square btn-ghost"
@ -42,6 +145,7 @@ export function TopNav({
> >
<Menu className="h-6 w-6" /> <Menu className="h-6 w-6" />
</button> </button>
) : null}
<span className="font-semibold text-base-content ml-1">OpenSEO</span> <span className="font-semibold text-base-content ml-1">OpenSEO</span>
</div> </div>
@ -51,17 +155,17 @@ export function TopNav({
</span> </span>
{projectId {projectId
? projectNavItems.map((item) => { ? projectNavItems.map((item) => {
const { icon: Icon, matchSegment, ...linkProps } = item; const { icon: Icon, ...linkProps } = item;
const isActive = pathname.includes(matchSegment);
return ( return (
<Link <Link
key={linkProps.to} key={linkProps.to}
{...linkProps} {...linkProps}
className={`btn btn-sm gap-2 ${ activeOptions={{ exact: false, includeSearch: false }}
isActive className="btn btn-sm gap-2 btn-ghost text-base-content/60 hover:text-base-content"
? "bg-primary/10 text-primary font-medium border-transparent" activeProps={{
: "btn-ghost text-base-content/60 hover:text-base-content" className:
}`} "bg-primary/10 text-primary font-medium border-transparent hover:text-primary",
}}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
{item.label} {item.label}
@ -117,15 +221,11 @@ function HostedSessionActions({
} }
const handleSignOut = () => { const handleSignOut = () => {
const redirectTo = getCurrentAuthRedirect(window.location); const signInHref = getSignInHrefForLocation(window.location);
void authClient.signOut({ void authClient.signOut({
fetchOptions: { fetchOptions: {
onSuccess: () => { onSuccess: () => {
window.location.assign( window.location.assign(signInHref);
redirectTo === "/"
? "/sign-in"
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`,
);
}, },
}, },
}); });
@ -165,162 +265,3 @@ function HostedSessionActions({
</div> </div>
); );
} }
export function SeoApiStatusBanners({
shouldShowSeoApiWarning,
seoApiKeyStatusError,
}: {
shouldShowSeoApiWarning: boolean;
seoApiKeyStatusError: boolean;
}) {
return (
<>
{shouldShowSeoApiWarning ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-warning">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
Setup needed: add your DataForSEO API key to use OpenSEO
features. See the quick steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
{seoApiKeyStatusError ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-info">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
We could not verify your DataForSEO setup. If features are not
working, check the setup steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
</>
);
}
export function AppContent({
drawerOpen,
pathname,
projectId,
onCloseDrawer,
}: {
drawerOpen: boolean;
pathname: string;
projectId: string | null;
onCloseDrawer: () => void;
}) {
return (
<>
<div className="flex-1 min-h-0 md:hidden">
<div className="h-full overflow-auto">
<Outlet />
</div>
{drawerOpen ? (
<div className="fixed inset-0 z-50">
<button
type="button"
aria-label="Close sidebar"
className="absolute inset-0 bg-black/45"
onClick={onCloseDrawer}
/>
<div className="absolute left-0 top-0 h-full">
<Sidebar
currentPath={pathname}
projectId={projectId}
onNavigate={onCloseDrawer}
onClose={onCloseDrawer}
/>
</div>
</div>
) : null}
</div>
<div className="hidden md:block flex-1 min-h-0 overflow-auto">
<Outlet />
</div>
</>
);
}
export const MissingSeoSetupModal = React.forwardRef<
HTMLDivElement,
{
isOpen: boolean;
onClose: () => void;
}
>(({ isOpen, onClose }, ref) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div
ref={ref}
role="dialog"
aria-modal="true"
aria-labelledby="dataforseo-setup-title"
aria-describedby="dataforseo-setup-description"
tabIndex={-1}
className="w-full max-w-lg rounded-xl border border-base-300 bg-base-100 p-5 shadow-2xl"
>
<div className="flex items-start gap-3">
<div className="rounded-full bg-warning/20 p-2 text-warning">
<AlertTriangle className="size-5" />
</div>
<div className="space-y-2">
<h2
id="dataforseo-setup-title"
className="text-lg font-semibold text-base-content"
>
One quick setup step
</h2>
<p
id="dataforseo-setup-description"
className="text-sm text-base-content/75"
>
Add your DataForSEO API key to start using OpenSEO.
</p>
</div>
</div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<button type="button" className="btn btn-ghost" onClick={onClose}>
Dismiss
</button>
<Link
{...dataforseoHelpLinkOptions}
className="btn btn-primary"
onClick={onClose}
>
Open setup guide
<ExternalLink className="size-4" />
</Link>
</div>
</div>
</div>
);
});
MissingSeoSetupModal.displayName = "MissingSeoSetupModal";

View File

@ -0,0 +1,163 @@
import * as React from "react";
import { Link } from "@tanstack/react-router";
import { AlertTriangle, ExternalLink } from "lucide-react";
import { Sidebar } from "@/client/components/Sidebar";
import { dataforseoHelpLinkOptions } from "@/client/navigation/items";
function SeoApiStatusBanners({
shouldShowSeoApiWarning,
seoApiKeyStatusError,
}: {
shouldShowSeoApiWarning: boolean;
seoApiKeyStatusError: boolean;
}) {
return (
<>
{shouldShowSeoApiWarning ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-warning">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
Setup needed: add your DataForSEO API key to use OpenSEO
features. See the quick steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
{seoApiKeyStatusError ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-info">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
We could not verify your DataForSEO setup. If features are not
working, check the setup steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
</>
);
}
function AppContent({
drawerOpen,
projectId,
onCloseDrawer,
children,
}: {
drawerOpen: boolean;
projectId: string | null;
onCloseDrawer: () => void;
children: React.ReactNode;
}) {
return (
<>
<div className="flex-1 min-h-0 md:hidden">
<div className="h-full overflow-auto">{children}</div>
{drawerOpen && projectId ? (
<div className="fixed inset-0 z-50">
<button
type="button"
aria-label="Close sidebar"
className="absolute inset-0 bg-black/45"
onClick={onCloseDrawer}
/>
<div className="absolute left-0 top-0 h-full">
<Sidebar
projectId={projectId}
onNavigate={onCloseDrawer}
onClose={onCloseDrawer}
/>
</div>
</div>
) : null}
</div>
<div className="hidden md:block flex-1 min-h-0 overflow-auto">
{children}
</div>
</>
);
}
const MissingSeoSetupModal = React.forwardRef<
HTMLDivElement,
{
isOpen: boolean;
onClose: () => void;
}
>(({ isOpen, onClose }, ref) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div
ref={ref}
role="dialog"
aria-modal="true"
aria-labelledby="dataforseo-setup-title"
aria-describedby="dataforseo-setup-description"
tabIndex={-1}
className="w-full max-w-lg rounded-xl border border-base-300 bg-base-100 p-5 shadow-2xl"
>
<div className="flex items-start gap-3">
<div className="rounded-full bg-warning/20 p-2 text-warning">
<AlertTriangle className="size-5" />
</div>
<div className="space-y-2">
<h2
id="dataforseo-setup-title"
className="text-lg font-semibold text-base-content"
>
One quick setup step
</h2>
<p
id="dataforseo-setup-description"
className="text-sm text-base-content/75"
>
Add your DataForSEO API key to start using OpenSEO.
</p>
</div>
</div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<button type="button" className="btn btn-ghost" onClick={onClose}>
Dismiss
</button>
<Link
{...dataforseoHelpLinkOptions}
className="btn btn-primary"
onClick={onClose}
>
Open setup guide
<ExternalLink className="size-4" />
</Link>
</div>
</div>
</div>
);
});
MissingSeoSetupModal.displayName = "MissingSeoSetupModal";
export { AppContent, MissingSeoSetupModal, SeoApiStatusBanners };

View File

@ -6,12 +6,32 @@ export function normalizeAuthRedirect(value: string | null | undefined) {
return value; return value;
} }
export function getCurrentAuthRedirect(location: { export function getCurrentAuthRedirectFromHref(href: string) {
const url = new URL(href, "https://openseo.local");
return normalizeAuthRedirect(`${url.pathname}${url.search}${url.hash}`);
}
export function getSignInSearch(redirectTo: string) {
return redirectTo === "/" ? {} : { redirect: redirectTo };
}
export function getSignInHref(redirectTo: string) {
const search = getSignInSearch(redirectTo);
if (!("redirect" in search)) {
return "/sign-in";
}
return `/sign-in?redirect=${encodeURIComponent(search.redirect ?? "/")}`;
}
export function getSignInHrefForLocation(location: {
pathname: string; pathname: string;
search: string; search: string;
hash?: string; hash?: string;
}) { }) {
return normalizeAuthRedirect( return getSignInHref(
normalizeAuthRedirect(
`${location.pathname}${location.search}${location.hash ?? ""}`, `${location.pathname}${location.search}${location.hash ?? ""}`,
),
); );
} }

View File

@ -10,36 +10,41 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as AuthRouteImport } from './routes/_auth' import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index' import { Route as ProjectRouteRouteImport } from './routes/_project/route'
import { Route as HelpDataforseoApiKeyRouteImport } from './routes/help/dataforseo-api-key' import { Route as AppRouteRouteImport } from './routes/_app/route'
import { Route as AppIndexRouteImport } from './routes/_app/index'
import { Route as AuthSignUpRouteImport } from './routes/_auth.sign-up' import { Route as AuthSignUpRouteImport } from './routes/_auth.sign-up'
import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in' import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in'
import { Route as PProjectIdRouteRouteImport } from './routes/p/$projectId/route'
import { Route as PProjectIdIndexRouteImport } from './routes/p/$projectId/index'
import { Route as PProjectIdSavedRouteImport } from './routes/p/$projectId/saved'
import { Route as PProjectIdKeywordsRouteImport } from './routes/p/$projectId/keywords'
import { Route as PProjectIdDomainRouteImport } from './routes/p/$projectId/domain'
import { Route as PProjectIdBacklinksRouteImport } from './routes/p/$projectId/backlinks'
import { Route as PProjectIdAuditRouteImport } from './routes/p/$projectId/audit'
import { Route as PProjectIdAiRouteImport } from './routes/p/$projectId/ai'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
import { Route as PProjectIdAuditIndexRouteImport } from './routes/p/$projectId/audit/index' import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
import { Route as PProjectIdAuditIssuesResultIdRouteImport } from './routes/p/$projectId/audit/issues/$resultId' import { Route as ProjectPProjectIdRouteRouteImport } from './routes/_project/p/$projectId/route'
import { Route as ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
import { Route as ProjectPProjectIdSavedRouteImport } from './routes/_project/p/$projectId/saved'
import { Route as ProjectPProjectIdKeywordsRouteImport } from './routes/_project/p/$projectId/keywords'
import { Route as ProjectPProjectIdDomainRouteImport } from './routes/_project/p/$projectId/domain'
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 ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index'
import { Route as ProjectPProjectIdAuditIssuesResultIdRouteImport } from './routes/_project/p/$projectId/audit/issues/$resultId'
const AuthRoute = AuthRouteImport.update({ const AuthRoute = AuthRouteImport.update({
id: '/_auth', id: '/_auth',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const IndexRoute = IndexRouteImport.update({ const ProjectRouteRoute = ProjectRouteRouteImport.update({
id: '/', id: '/_project',
path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const HelpDataforseoApiKeyRoute = HelpDataforseoApiKeyRouteImport.update({ const AppRouteRoute = AppRouteRouteImport.update({
id: '/help/dataforseo-api-key', id: '/_app',
path: '/help/dataforseo-api-key',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AppIndexRoute = AppIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AppRouteRoute,
} as any)
const AuthSignUpRoute = AuthSignUpRouteImport.update({ const AuthSignUpRoute = AuthSignUpRouteImport.update({
id: '/sign-up', id: '/sign-up',
path: '/sign-up', path: '/sign-up',
@ -50,121 +55,131 @@ const AuthSignInRoute = AuthSignInRouteImport.update({
path: '/sign-in', path: '/sign-in',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const PProjectIdRouteRoute = PProjectIdRouteRouteImport.update({
id: '/p/$projectId',
path: '/p/$projectId',
getParentRoute: () => rootRouteImport,
} as any)
const PProjectIdIndexRoute = PProjectIdIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdSavedRoute = PProjectIdSavedRouteImport.update({
id: '/saved',
path: '/saved',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdKeywordsRoute = PProjectIdKeywordsRouteImport.update({
id: '/keywords',
path: '/keywords',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdDomainRoute = PProjectIdDomainRouteImport.update({
id: '/domain',
path: '/domain',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdBacklinksRoute = PProjectIdBacklinksRouteImport.update({
id: '/backlinks',
path: '/backlinks',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdAuditRoute = PProjectIdAuditRouteImport.update({
id: '/audit',
path: '/audit',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdAiRoute = PProjectIdAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$', id: '/api/auth/$',
path: '/api/auth/$', path: '/api/auth/$',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const PProjectIdAuditIndexRoute = PProjectIdAuditIndexRouteImport.update({ const AppHelpDataforseoApiKeyRoute = AppHelpDataforseoApiKeyRouteImport.update({
id: '/help/dataforseo-api-key',
path: '/help/dataforseo-api-key',
getParentRoute: () => AppRouteRoute,
} as any)
const ProjectPProjectIdRouteRoute = ProjectPProjectIdRouteRouteImport.update({
id: '/p/$projectId',
path: '/p/$projectId',
getParentRoute: () => ProjectRouteRoute,
} as any)
const ProjectPProjectIdIndexRoute = ProjectPProjectIdIndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => PProjectIdAuditRoute, getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any) } as any)
const PProjectIdAuditIssuesResultIdRoute = const ProjectPProjectIdSavedRoute = ProjectPProjectIdSavedRouteImport.update({
PProjectIdAuditIssuesResultIdRouteImport.update({ id: '/saved',
path: '/saved',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdKeywordsRoute =
ProjectPProjectIdKeywordsRouteImport.update({
id: '/keywords',
path: '/keywords',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdDomainRoute = ProjectPProjectIdDomainRouteImport.update({
id: '/domain',
path: '/domain',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdBacklinksRoute =
ProjectPProjectIdBacklinksRouteImport.update({
id: '/backlinks',
path: '/backlinks',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAuditRoute = ProjectPProjectIdAuditRouteImport.update({
id: '/audit',
path: '/audit',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAuditIndexRoute =
ProjectPProjectIdAuditIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => ProjectPProjectIdAuditRoute,
} as any)
const ProjectPProjectIdAuditIssuesResultIdRoute =
ProjectPProjectIdAuditIssuesResultIdRouteImport.update({
id: '/issues/$resultId', id: '/issues/$resultId',
path: '/issues/$resultId', path: '/issues/$resultId',
getParentRoute: () => PProjectIdAuditRoute, getParentRoute: () => ProjectPProjectIdAuditRoute,
} as any) } as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof AppIndexRoute
'/p/$projectId': typeof PProjectIdRouteRouteWithChildren
'/sign-in': typeof AuthSignInRoute '/sign-in': typeof AuthSignInRoute
'/sign-up': typeof AuthSignUpRoute '/sign-up': typeof AuthSignUpRoute
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute '/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/p/$projectId/ai': typeof PProjectIdAiRoute '/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/audit': typeof PProjectIdAuditRouteWithChildren '/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute '/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof PProjectIdDomainRoute '/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute '/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
'/p/$projectId/saved': typeof PProjectIdSavedRoute '/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId/': typeof PProjectIdIndexRoute '/p/$projectId/': typeof ProjectPProjectIdIndexRoute
'/p/$projectId/audit/': typeof PProjectIdAuditIndexRoute '/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute '/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof AppIndexRoute
'/sign-in': typeof AuthSignInRoute '/sign-in': typeof AuthSignInRoute
'/sign-up': typeof AuthSignUpRoute '/sign-up': typeof AuthSignUpRoute
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute '/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/p/$projectId/ai': typeof PProjectIdAiRoute '/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute '/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof PProjectIdDomainRoute '/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute '/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
'/p/$projectId/saved': typeof PProjectIdSavedRoute '/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId': typeof PProjectIdIndexRoute '/p/$projectId': typeof ProjectPProjectIdIndexRoute
'/p/$projectId/audit': typeof PProjectIdAuditIndexRoute '/p/$projectId/audit': typeof ProjectPProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute '/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/_app': typeof AppRouteRouteWithChildren
'/_project': typeof ProjectRouteRouteWithChildren
'/_auth': typeof AuthRouteWithChildren '/_auth': typeof AuthRouteWithChildren
'/p/$projectId': typeof PProjectIdRouteRouteWithChildren
'/_auth/sign-in': typeof AuthSignInRoute '/_auth/sign-in': typeof AuthSignInRoute
'/_auth/sign-up': typeof AuthSignUpRoute '/_auth/sign-up': typeof AuthSignUpRoute
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute '/_app/': typeof AppIndexRoute
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/p/$projectId/ai': typeof PProjectIdAiRoute '/_project/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/audit': typeof PProjectIdAuditRouteWithChildren '/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute '/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof PProjectIdDomainRoute '/_project/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute '/_project/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
'/p/$projectId/saved': typeof PProjectIdSavedRoute '/_project/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId/': typeof PProjectIdIndexRoute '/_project/p/$projectId/': typeof ProjectPProjectIdIndexRoute
'/p/$projectId/audit/': typeof PProjectIdAuditIndexRoute '/_project/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute '/_project/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/p/$projectId'
| '/sign-in' | '/sign-in'
| '/sign-up' | '/sign-up'
| '/p/$projectId'
| '/help/dataforseo-api-key' | '/help/dataforseo-api-key'
| '/api/auth/$' | '/api/auth/$'
| '/p/$projectId/ai' | '/p/$projectId/ai'
@ -193,29 +208,30 @@ export interface FileRouteTypes {
| '/p/$projectId/audit/issues/$resultId' | '/p/$projectId/audit/issues/$resultId'
id: id:
| '__root__' | '__root__'
| '/' | '/_app'
| '/_project'
| '/_auth' | '/_auth'
| '/p/$projectId'
| '/_auth/sign-in' | '/_auth/sign-in'
| '/_auth/sign-up' | '/_auth/sign-up'
| '/help/dataforseo-api-key' | '/_app/'
| '/_project/p/$projectId'
| '/_app/help/dataforseo-api-key'
| '/api/auth/$' | '/api/auth/$'
| '/p/$projectId/ai' | '/_project/p/$projectId/ai'
| '/p/$projectId/audit' | '/_project/p/$projectId/audit'
| '/p/$projectId/backlinks' | '/_project/p/$projectId/backlinks'
| '/p/$projectId/domain' | '/_project/p/$projectId/domain'
| '/p/$projectId/keywords' | '/_project/p/$projectId/keywords'
| '/p/$projectId/saved' | '/_project/p/$projectId/saved'
| '/p/$projectId/' | '/_project/p/$projectId/'
| '/p/$projectId/audit/' | '/_project/p/$projectId/audit/'
| '/p/$projectId/audit/issues/$resultId' | '/_project/p/$projectId/audit/issues/$resultId'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute AppRouteRoute: typeof AppRouteRouteWithChildren
ProjectRouteRoute: typeof ProjectRouteRouteWithChildren
AuthRoute: typeof AuthRouteWithChildren AuthRoute: typeof AuthRouteWithChildren
PProjectIdRouteRoute: typeof PProjectIdRouteRouteWithChildren
HelpDataforseoApiKeyRoute: typeof HelpDataforseoApiKeyRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute
} }
@ -228,20 +244,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthRouteImport preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/': { '/_project': {
id: '/' id: '/_project'
path: '/' path: ''
fullPath: '/' fullPath: '/'
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof ProjectRouteRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/help/dataforseo-api-key': { '/_app': {
id: '/help/dataforseo-api-key' id: '/_app'
path: '/help/dataforseo-api-key' path: ''
fullPath: '/help/dataforseo-api-key' fullPath: '/'
preLoaderRoute: typeof HelpDataforseoApiKeyRouteImport preLoaderRoute: typeof AppRouteRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_app/': {
id: '/_app/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof AppIndexRouteImport
parentRoute: typeof AppRouteRoute
}
'/_auth/sign-up': { '/_auth/sign-up': {
id: '/_auth/sign-up' id: '/_auth/sign-up'
path: '/sign-up' path: '/sign-up'
@ -256,62 +279,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthSignInRouteImport preLoaderRoute: typeof AuthSignInRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/p/$projectId': {
id: '/p/$projectId'
path: '/p/$projectId'
fullPath: '/p/$projectId'
preLoaderRoute: typeof PProjectIdRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/p/$projectId/': {
id: '/p/$projectId/'
path: '/'
fullPath: '/p/$projectId/'
preLoaderRoute: typeof PProjectIdIndexRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/saved': {
id: '/p/$projectId/saved'
path: '/saved'
fullPath: '/p/$projectId/saved'
preLoaderRoute: typeof PProjectIdSavedRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/keywords': {
id: '/p/$projectId/keywords'
path: '/keywords'
fullPath: '/p/$projectId/keywords'
preLoaderRoute: typeof PProjectIdKeywordsRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/domain': {
id: '/p/$projectId/domain'
path: '/domain'
fullPath: '/p/$projectId/domain'
preLoaderRoute: typeof PProjectIdDomainRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/backlinks': {
id: '/p/$projectId/backlinks'
path: '/backlinks'
fullPath: '/p/$projectId/backlinks'
preLoaderRoute: typeof PProjectIdBacklinksRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/audit': {
id: '/p/$projectId/audit'
path: '/audit'
fullPath: '/p/$projectId/audit'
preLoaderRoute: typeof PProjectIdAuditRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/ai': {
id: '/p/$projectId/ai'
path: '/ai'
fullPath: '/p/$projectId/ai'
preLoaderRoute: typeof PProjectIdAiRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/api/auth/$': { '/api/auth/$': {
id: '/api/auth/$' id: '/api/auth/$'
path: '/api/auth/$' path: '/api/auth/$'
@ -319,23 +286,155 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiAuthSplatRouteImport preLoaderRoute: typeof ApiAuthSplatRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/p/$projectId/audit/': { '/_app/help/dataforseo-api-key': {
id: '/p/$projectId/audit/' id: '/_app/help/dataforseo-api-key'
path: '/help/dataforseo-api-key'
fullPath: '/help/dataforseo-api-key'
preLoaderRoute: typeof AppHelpDataforseoApiKeyRouteImport
parentRoute: typeof AppRouteRoute
}
'/_project/p/$projectId': {
id: '/_project/p/$projectId'
path: '/p/$projectId'
fullPath: '/p/$projectId'
preLoaderRoute: typeof ProjectPProjectIdRouteRouteImport
parentRoute: typeof ProjectRouteRoute
}
'/_project/p/$projectId/': {
id: '/_project/p/$projectId/'
path: '/'
fullPath: '/p/$projectId/'
preLoaderRoute: typeof ProjectPProjectIdIndexRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/saved': {
id: '/_project/p/$projectId/saved'
path: '/saved'
fullPath: '/p/$projectId/saved'
preLoaderRoute: typeof ProjectPProjectIdSavedRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/keywords': {
id: '/_project/p/$projectId/keywords'
path: '/keywords'
fullPath: '/p/$projectId/keywords'
preLoaderRoute: typeof ProjectPProjectIdKeywordsRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/domain': {
id: '/_project/p/$projectId/domain'
path: '/domain'
fullPath: '/p/$projectId/domain'
preLoaderRoute: typeof ProjectPProjectIdDomainRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/backlinks': {
id: '/_project/p/$projectId/backlinks'
path: '/backlinks'
fullPath: '/p/$projectId/backlinks'
preLoaderRoute: typeof ProjectPProjectIdBacklinksRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/audit': {
id: '/_project/p/$projectId/audit'
path: '/audit'
fullPath: '/p/$projectId/audit'
preLoaderRoute: typeof ProjectPProjectIdAuditRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/ai': {
id: '/_project/p/$projectId/ai'
path: '/ai'
fullPath: '/p/$projectId/ai'
preLoaderRoute: typeof ProjectPProjectIdAiRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/audit/': {
id: '/_project/p/$projectId/audit/'
path: '/' path: '/'
fullPath: '/p/$projectId/audit/' fullPath: '/p/$projectId/audit/'
preLoaderRoute: typeof PProjectIdAuditIndexRouteImport preLoaderRoute: typeof ProjectPProjectIdAuditIndexRouteImport
parentRoute: typeof PProjectIdAuditRoute parentRoute: typeof ProjectPProjectIdAuditRoute
} }
'/p/$projectId/audit/issues/$resultId': { '/_project/p/$projectId/audit/issues/$resultId': {
id: '/p/$projectId/audit/issues/$resultId' id: '/_project/p/$projectId/audit/issues/$resultId'
path: '/issues/$resultId' path: '/issues/$resultId'
fullPath: '/p/$projectId/audit/issues/$resultId' fullPath: '/p/$projectId/audit/issues/$resultId'
preLoaderRoute: typeof PProjectIdAuditIssuesResultIdRouteImport preLoaderRoute: typeof ProjectPProjectIdAuditIssuesResultIdRouteImport
parentRoute: typeof PProjectIdAuditRoute parentRoute: typeof ProjectPProjectIdAuditRoute
} }
} }
} }
interface AppRouteRouteChildren {
AppIndexRoute: typeof AppIndexRoute
AppHelpDataforseoApiKeyRoute: typeof AppHelpDataforseoApiKeyRoute
}
const AppRouteRouteChildren: AppRouteRouteChildren = {
AppIndexRoute: AppIndexRoute,
AppHelpDataforseoApiKeyRoute: AppHelpDataforseoApiKeyRoute,
}
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
AppRouteRouteChildren,
)
interface ProjectPProjectIdAuditRouteChildren {
ProjectPProjectIdAuditIndexRoute: typeof ProjectPProjectIdAuditIndexRoute
ProjectPProjectIdAuditIssuesResultIdRoute: typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
const ProjectPProjectIdAuditRouteChildren: ProjectPProjectIdAuditRouteChildren =
{
ProjectPProjectIdAuditIndexRoute: ProjectPProjectIdAuditIndexRoute,
ProjectPProjectIdAuditIssuesResultIdRoute:
ProjectPProjectIdAuditIssuesResultIdRoute,
}
const ProjectPProjectIdAuditRouteWithChildren =
ProjectPProjectIdAuditRoute._addFileChildren(
ProjectPProjectIdAuditRouteChildren,
)
interface ProjectPProjectIdRouteRouteChildren {
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
ProjectPProjectIdDomainRoute: typeof ProjectPProjectIdDomainRoute
ProjectPProjectIdKeywordsRoute: typeof ProjectPProjectIdKeywordsRoute
ProjectPProjectIdSavedRoute: typeof ProjectPProjectIdSavedRoute
ProjectPProjectIdIndexRoute: typeof ProjectPProjectIdIndexRoute
}
const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren =
{
ProjectPProjectIdAiRoute: ProjectPProjectIdAiRoute,
ProjectPProjectIdAuditRoute: ProjectPProjectIdAuditRouteWithChildren,
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
ProjectPProjectIdDomainRoute: ProjectPProjectIdDomainRoute,
ProjectPProjectIdKeywordsRoute: ProjectPProjectIdKeywordsRoute,
ProjectPProjectIdSavedRoute: ProjectPProjectIdSavedRoute,
ProjectPProjectIdIndexRoute: ProjectPProjectIdIndexRoute,
}
const ProjectPProjectIdRouteRouteWithChildren =
ProjectPProjectIdRouteRoute._addFileChildren(
ProjectPProjectIdRouteRouteChildren,
)
interface ProjectRouteRouteChildren {
ProjectPProjectIdRouteRoute: typeof ProjectPProjectIdRouteRouteWithChildren
}
const ProjectRouteRouteChildren: ProjectRouteRouteChildren = {
ProjectPProjectIdRouteRoute: ProjectPProjectIdRouteRouteWithChildren,
}
const ProjectRouteRouteWithChildren = ProjectRouteRoute._addFileChildren(
ProjectRouteRouteChildren,
)
interface AuthRouteChildren { interface AuthRouteChildren {
AuthSignInRoute: typeof AuthSignInRoute AuthSignInRoute: typeof AuthSignInRoute
AuthSignUpRoute: typeof AuthSignUpRoute AuthSignUpRoute: typeof AuthSignUpRoute
@ -348,49 +447,10 @@ const AuthRouteChildren: AuthRouteChildren = {
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
interface PProjectIdAuditRouteChildren {
PProjectIdAuditIndexRoute: typeof PProjectIdAuditIndexRoute
PProjectIdAuditIssuesResultIdRoute: typeof PProjectIdAuditIssuesResultIdRoute
}
const PProjectIdAuditRouteChildren: PProjectIdAuditRouteChildren = {
PProjectIdAuditIndexRoute: PProjectIdAuditIndexRoute,
PProjectIdAuditIssuesResultIdRoute: PProjectIdAuditIssuesResultIdRoute,
}
const PProjectIdAuditRouteWithChildren = PProjectIdAuditRoute._addFileChildren(
PProjectIdAuditRouteChildren,
)
interface PProjectIdRouteRouteChildren {
PProjectIdAiRoute: typeof PProjectIdAiRoute
PProjectIdAuditRoute: typeof PProjectIdAuditRouteWithChildren
PProjectIdBacklinksRoute: typeof PProjectIdBacklinksRoute
PProjectIdDomainRoute: typeof PProjectIdDomainRoute
PProjectIdKeywordsRoute: typeof PProjectIdKeywordsRoute
PProjectIdSavedRoute: typeof PProjectIdSavedRoute
PProjectIdIndexRoute: typeof PProjectIdIndexRoute
}
const PProjectIdRouteRouteChildren: PProjectIdRouteRouteChildren = {
PProjectIdAiRoute: PProjectIdAiRoute,
PProjectIdAuditRoute: PProjectIdAuditRouteWithChildren,
PProjectIdBacklinksRoute: PProjectIdBacklinksRoute,
PProjectIdDomainRoute: PProjectIdDomainRoute,
PProjectIdKeywordsRoute: PProjectIdKeywordsRoute,
PProjectIdSavedRoute: PProjectIdSavedRoute,
PProjectIdIndexRoute: PProjectIdIndexRoute,
}
const PProjectIdRouteRouteWithChildren = PProjectIdRouteRoute._addFileChildren(
PProjectIdRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, AppRouteRoute: AppRouteRouteWithChildren,
ProjectRouteRoute: ProjectRouteRouteWithChildren,
AuthRoute: AuthRouteWithChildren, AuthRoute: AuthRouteWithChildren,
PProjectIdRouteRoute: PProjectIdRouteRouteWithChildren,
HelpDataforseoApiKeyRoute: HelpDataforseoApiKeyRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute, ApiAuthSplatRoute: ApiAuthSplatRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@ -5,28 +5,16 @@ import {
Outlet, Outlet,
Scripts, Scripts,
createRootRoute, createRootRoute,
useLocation,
useMatches,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { TanStackDevtools } from "@tanstack/react-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import * as React from "react"; import * as React from "react";
import { useState } from "react";
import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary"; import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary";
import { NotFound } from "@/client/components/NotFound"; import { NotFound } from "@/client/components/NotFound";
import appCss from "@/client/styles/app.css?url"; import appCss from "@/client/styles/app.css?url";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { queryClient } from "@/client/tanstack-db"; import { queryClient } from "@/client/tanstack-db";
import { getSeoApiKeyStatus } from "@/serverFunctions/config";
import {
AppContent,
MissingSeoSetupModal,
SeoApiStatusBanners,
TopNav,
} from "@/client/layout/AppShell";
const DATAFORSEO_HELP_PATH = "/help/dataforseo-api-key";
export const Route = createRootRoute({ export const Route = createRootRoute({
head: () => ({ head: () => ({
@ -77,112 +65,7 @@ export const Route = createRootRoute({
}); });
function AppLayout() { function AppLayout() {
const isAuthLayout = useMatches({
select: (matches) => matches.some((match) => match.routeId === "/_auth"),
});
if (isAuthLayout) {
return <Outlet />; return <Outlet />;
}
return <AppShellLayout />;
}
function AppShellLayout() {
const location = useLocation();
const [drawerOpen, setDrawerOpen] = useState(false);
const setupModalRef = React.useRef<HTMLDivElement | null>(null);
const [isSeoApiKeyConfigured, setIsSeoApiKeyConfigured] = useState<
boolean | null
>(null);
const [seoApiKeyStatusError, setSeoApiKeyStatusError] = useState(false);
const [showMissingSeoApiKeyModal, setShowMissingSeoApiKeyModal] =
useState(false);
const projectIdMatch = location.pathname.match(/^\/p\/([^/]+)/);
const projectId = projectIdMatch?.[1] ?? null;
React.useEffect(() => {
let cancelled = false;
const checkSeoApiKeyStatus = async () => {
try {
const result = await getSeoApiKeyStatus();
if (cancelled) return;
setSeoApiKeyStatusError(false);
setIsSeoApiKeyConfigured(result.configured);
if (!result.configured) {
setShowMissingSeoApiKeyModal(true);
}
} catch {
if (cancelled) return;
setSeoApiKeyStatusError(true);
setIsSeoApiKeyConfigured(null);
setShowMissingSeoApiKeyModal(false);
}
};
void checkSeoApiKeyStatus();
return () => {
cancelled = true;
};
}, []);
const shouldShowMissingSeoApiKeyModal =
showMissingSeoApiKeyModal && location.pathname !== DATAFORSEO_HELP_PATH;
const shouldShowSeoApiWarning =
!seoApiKeyStatusError &&
isSeoApiKeyConfigured === false &&
!shouldShowMissingSeoApiKeyModal;
React.useEffect(() => {
if (!shouldShowMissingSeoApiKeyModal) return;
setupModalRef.current?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setShowMissingSeoApiKeyModal(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [shouldShowMissingSeoApiKeyModal]);
return (
<div className="flex flex-col h-[100dvh] bg-base-200">
<TopNav
drawerOpen={drawerOpen}
projectId={projectId}
pathname={location.pathname}
onOpenDrawer={() => setDrawerOpen(true)}
/>
<SeoApiStatusBanners
shouldShowSeoApiWarning={shouldShowSeoApiWarning}
seoApiKeyStatusError={seoApiKeyStatusError}
/>
<AppContent
drawerOpen={drawerOpen}
pathname={location.pathname}
projectId={projectId}
onCloseDrawer={() => setDrawerOpen(false)}
/>
<MissingSeoSetupModal
ref={setupModalRef}
isOpen={shouldShowMissingSeoApiKeyModal}
onClose={() => setShowMissingSeoApiKeyModal(false)}
/>
</div>
);
} }
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {

View File

@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
const DATAFORSEO_API_ACCESS_URL = "https://app.dataforseo.com/api-access"; const DATAFORSEO_API_ACCESS_URL = "https://app.dataforseo.com/api-access";
export const Route = createFileRoute("/help/dataforseo-api-key")({ export const Route = createFileRoute("/_app/help/dataforseo-api-key")({
component: DataforseoApiKeyHelpPage, component: DataforseoApiKeyHelpPage,
}); });

View File

@ -9,7 +9,7 @@ import {
import { AuthConfigErrorCard } from "@/client/components/AuthConfigErrorCard"; import { AuthConfigErrorCard } from "@/client/components/AuthConfigErrorCard";
import { UnauthenticatedErrorCard } from "@/client/components/UnauthenticatedErrorCard"; import { UnauthenticatedErrorCard } from "@/client/components/UnauthenticatedErrorCard";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/_app/")({
component: IndexRedirect, component: IndexRedirect,
}); });

14
src/routes/_app/route.tsx Normal file
View File

@ -0,0 +1,14 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { AuthenticatedAppLayout } from "@/client/layout/AppShell";
export const Route = createFileRoute("/_app")({
component: AppRouteLayout,
});
function AppRouteLayout() {
return (
<AuthenticatedAppLayout>
<Outlet />
</AuthenticatedAppLayout>
);
}

View File

@ -3,10 +3,10 @@ import { Link, createFileRoute } from "@tanstack/react-router";
import { import {
AuthPageCard, AuthPageCard,
authRedirectSearchSchema, authRedirectSearchSchema,
getAuthLinkSearch,
useAuthPageState, useAuthPageState,
} from "@/client/features/auth/AuthPage"; } from "@/client/features/auth/AuthPage";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { getSignInSearch } from "@/lib/auth-redirect";
import { z } from "zod"; import { z } from "zod";
const signInSchema = z.object({ const signInSchema = z.object({
@ -96,7 +96,7 @@ function SignInPage() {
Need an account?{" "} Need an account?{" "}
<Link <Link
to="/sign-up" to="/sign-up"
search={getAuthLinkSearch(redirectTo)} search={getSignInSearch(redirectTo)}
className="link link-primary" className="link link-primary"
> >
Create account Create account

View File

@ -3,11 +3,11 @@ import { Link, createFileRoute } from "@tanstack/react-router";
import { import {
AuthPageCard, AuthPageCard,
authRedirectSearchSchema, authRedirectSearchSchema,
getAuthLinkSearch,
getFieldError, getFieldError,
useAuthPageState, useAuthPageState,
} from "@/client/features/auth/AuthPage"; } from "@/client/features/auth/AuthPage";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { getSignInSearch } from "@/lib/auth-redirect";
import { import {
HOSTED_PASSWORD_MAX_LENGTH, HOSTED_PASSWORD_MAX_LENGTH,
HOSTED_PASSWORD_MIN_LENGTH, HOSTED_PASSWORD_MIN_LENGTH,
@ -130,7 +130,7 @@ function SignUpPage() {
Already have an account?{" "} Already have an account?{" "}
<Link <Link
to="/sign-in" to="/sign-in"
search={getAuthLinkSearch(redirectTo)} search={getSignInSearch(redirectTo)}
className="link link-primary" className="link link-primary"
> >
Sign in Sign in

View File

@ -1,10 +1,34 @@
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { authRedirectSearchSchema } from "@/client/features/auth/AuthPage";
import { useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
import { normalizeAuthRedirect } from "@/lib/auth-redirect";
export const Route = createFileRoute("/_auth")({ export const Route = createFileRoute("/_auth")({
component: AuthLayout, validateSearch: authRedirectSearchSchema,
component: AuthPageLayout,
}); });
function AuthLayout() { function AuthPageLayout() {
const search = Route.useSearch();
const navigate = useNavigate();
const { data: session, isPending } = useSession();
const isHostedMode = isHostedClientAuthMode();
const redirectTo = normalizeAuthRedirect(search.redirect);
useEffect(() => {
if (!session?.user?.id) {
return;
}
void navigate({ href: redirectTo, replace: true });
}, [navigate, redirectTo, session?.user?.id]);
if (isHostedMode && (isPending || session?.user?.id)) {
return null;
}
return ( return (
<div className="min-h-[100dvh] bg-base-200"> <div className="min-h-[100dvh] bg-base-200">
<div className="min-h-[100dvh] flex items-center justify-center p-4"> <div className="min-h-[100dvh] flex items-center justify-center p-4">

View File

@ -6,7 +6,7 @@ const SUPPORT_EMAIL = "ben@everyapp.com";
const DATAFORSEO_MCP_DOCS_URL = const DATAFORSEO_MCP_DOCS_URL =
"https://dataforseo.com/help-center/setting-up-the-official-dataforseo-mcp-server-simple-guide"; "https://dataforseo.com/help-center/setting-up-the-official-dataforseo-mcp-server-simple-guide";
export const Route = createFileRoute("/p/$projectId/ai")({ export const Route = createFileRoute("/_project/p/$projectId/ai")({
component: AiPage, component: AiPage,
}); });

View File

@ -1,6 +1,6 @@
import { createFileRoute, Outlet } from "@tanstack/react-router"; import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/p/$projectId/audit")({ export const Route = createFileRoute("/_project/p/$projectId/audit")({
component: SiteAuditLayout, component: SiteAuditLayout,
}); });

View File

@ -19,8 +19,8 @@ import {
SUPPORT_URL, SUPPORT_URL,
} from "@/client/features/audit/shared"; } from "@/client/features/audit/shared";
export const Route = createFileRoute<"/p/$projectId/audit/">( export const Route = createFileRoute<"/_project/p/$projectId/audit/">(
"/p/$projectId/audit/", "/_project/p/$projectId/audit/",
)({ )({
validateSearch: auditSearchSchema, validateSearch: auditSearchSchema,
component: SiteAuditPage, component: SiteAuditPage,

View File

@ -2,7 +2,9 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { PsiIssuesScreen } from "@/client/features/psi/issues/PsiIssuesScreen"; import { PsiIssuesScreen } from "@/client/features/psi/issues/PsiIssuesScreen";
import { psiIssuesSearchSchema } from "@/types/schemas/psi"; import { psiIssuesSearchSchema } from "@/types/schemas/psi";
export const Route = createFileRoute("/p/$projectId/audit/issues/$resultId")({ export const Route = createFileRoute(
"/_project/p/$projectId/audit/issues/$resultId",
)({
validateSearch: psiIssuesSearchSchema, validateSearch: psiIssuesSearchSchema,
component: AuditIssuesPage, component: AuditIssuesPage,
}); });

View File

@ -3,7 +3,7 @@ import { BacklinksPage } from "@/client/features/backlinks/BacklinksPage";
import { inferBacklinksSearchScopeFromTarget } from "@/client/features/backlinks/backlinksSearchScope"; import { inferBacklinksSearchScopeFromTarget } from "@/client/features/backlinks/backlinksSearchScope";
import { backlinksSearchSchema } from "@/types/schemas/backlinks"; import { backlinksSearchSchema } from "@/types/schemas/backlinks";
export const Route = createFileRoute("/p/$projectId/backlinks")({ export const Route = createFileRoute("/_project/p/$projectId/backlinks")({
validateSearch: backlinksSearchSchema, validateSearch: backlinksSearchSchema,
component: BacklinksRoute, component: BacklinksRoute,
}); });

View File

@ -7,7 +7,7 @@ import {
} from "@/client/features/domain/utils"; } from "@/client/features/domain/utils";
import { domainSearchSchema } from "@/types/schemas/domain"; import { domainSearchSchema } from "@/types/schemas/domain";
export const Route = createFileRoute("/p/$projectId/domain")({ export const Route = createFileRoute("/_project/p/$projectId/domain")({
validateSearch: domainSearchSchema, validateSearch: domainSearchSchema,
component: DomainOverviewRoute, component: DomainOverviewRoute,
}); });

View File

@ -1,6 +1,6 @@
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/p/$projectId/")({ export const Route = createFileRoute("/_project/p/$projectId/")({
beforeLoad: ({ params }) => { beforeLoad: ({ params }) => {
throw redirect({ throw redirect({
to: "/p/$projectId/keywords", to: "/p/$projectId/keywords",

View File

@ -10,7 +10,7 @@ import {
} from "@/client/features/keywords/keywordSearchParams"; } from "@/client/features/keywords/keywordSearchParams";
import { keywordsSearchSchema } from "@/types/schemas/keywords"; import { keywordsSearchSchema } from "@/types/schemas/keywords";
export const Route = createFileRoute("/p/$projectId/keywords")({ export const Route = createFileRoute("/_project/p/$projectId/keywords")({
validateSearch: keywordsSearchSchema, validateSearch: keywordsSearchSchema,
beforeLoad: ({ params, search }) => { beforeLoad: ({ params, search }) => {
const { normalized, changed } = normalizeLegacyKeywordSearch(search); const { normalized, changed } = normalizeLegacyKeywordSearch(search);

View File

@ -0,0 +1,48 @@
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { getErrorCode } from "@/client/lib/error-messages";
import { AuthenticatedAppLayout } from "@/client/layout/AppShell";
import {
getCurrentAuthRedirectFromHref,
getSignInSearch,
} from "@/lib/auth-redirect";
import { getProjectAccess } from "@/serverFunctions/projects";
export const Route = createFileRoute("/_project/p/$projectId")({
beforeLoad: async ({ location, params }) => {
try {
await getProjectAccess({ data: { projectId: params.projectId } });
} catch (error) {
if (getErrorCode(error) === "UNAUTHENTICATED") {
throw redirect({
to: "/sign-in",
search: getSignInSearch(
getCurrentAuthRedirectFromHref(location.href),
),
replace: true,
});
}
throw redirect({ to: "/", replace: true });
}
},
pendingComponent: ProjectRoutePending,
component: ProjectLayout,
});
function ProjectLayout() {
const { projectId } = Route.useParams();
return (
<AuthenticatedAppLayout projectId={projectId}>
<Outlet />
</AuthenticatedAppLayout>
);
}
function ProjectRoutePending() {
return (
<div className="flex h-full items-center justify-center">
<span className="loading loading-spinner loading-md" />
</div>
);
}

View File

@ -10,7 +10,7 @@ import { Trash2, Download, Search, Loader2, AlertCircle } from "lucide-react";
import { buildCsv, downloadCsv } from "@/client/lib/csv"; import { buildCsv, downloadCsv } from "@/client/lib/csv";
import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { getStandardErrorMessage } from "@/client/lib/error-messages";
export const Route = createFileRoute("/p/$projectId/saved")({ export const Route = createFileRoute("/_project/p/$projectId/saved")({
component: SavedKeywordsPage, component: SavedKeywordsPage,
}); });

View File

@ -0,0 +1,9 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_project")({
component: ProjectRouteLayout,
});
function ProjectRouteLayout() {
return <Outlet />;
}

View File

@ -1,41 +0,0 @@
import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { getProjectAccess } from "@/serverFunctions/projects";
export const Route = createFileRoute("/p/$projectId")({
component: ProjectLayout,
});
function ProjectLayout() {
const params = Route.useParams();
const { projectId } = params;
const navigate = useNavigate();
const {
data: project,
isLoading,
isError,
} = useQuery({
queryKey: ["project", projectId],
queryFn: () => getProjectAccess({ data: { projectId } }),
});
useEffect(() => {
if (!isLoading && (isError || !project)) {
void navigate({ to: "/" });
}
}, [isLoading, isError, project, navigate]);
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<span className="loading loading-spinner loading-md" />
</div>
);
}
if (!project) return null;
return <Outlet />;
}