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:
parent
4040a854a7
commit
bfc6a97466
@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
|
||||||
|
|||||||
163
src/client/layout/AppShellParts.tsx
Normal file
163
src/client/layout/AppShellParts.tsx
Normal 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 };
|
||||||
@ -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 ?? ""}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 }) {
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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
14
src/routes/_app/route.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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,
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
@ -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",
|
||||||
@ -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);
|
||||||
48
src/routes/_project/p/$projectId/route.tsx
Normal file
48
src/routes/_project/p/$projectId/route.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
9
src/routes/_project/route.tsx
Normal file
9
src/routes/_project/route.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_project")({
|
||||||
|
component: ProjectRouteLayout,
|
||||||
|
});
|
||||||
|
|
||||||
|
function ProjectRouteLayout() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@ -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 />;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user