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";
|
||||
|
||||
interface SidebarProps {
|
||||
currentPath: string;
|
||||
projectId: string | null;
|
||||
projectId: string;
|
||||
onNavigate?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar({ projectId, onNavigate, onClose }: SidebarProps) {
|
||||
const projectNavItems = getProjectNavItems(projectId);
|
||||
|
||||
return (
|
||||
@ -73,25 +43,26 @@ export function Sidebar({
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 py-4 pl-3 overflow-y-auto">
|
||||
{projectNavItems.map((item) => {
|
||||
const { icon: Icon, matchSegment, ...linkProps } = item;
|
||||
const isActive = currentPath.includes(matchSegment);
|
||||
const { icon: Icon, ...linkProps } = item;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={linkProps.to}
|
||||
{...linkProps}
|
||||
onClick={onNavigate}
|
||||
className={`relative flex items-center gap-3 pl-4 pr-4 py-2 text-sm transition-colors ${
|
||||
isActive
|
||||
? "text-base-content font-medium"
|
||||
: "text-base-content/60 hover:text-base-content hover:bg-base-200"
|
||||
}`}
|
||||
activeOptions={{ exact: false, includeSearch: false }}
|
||||
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"
|
||||
activeProps={{ className: "text-base-content font-medium" }}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1 bottom-1 w-[3px] bg-primary rounded-r-full" />
|
||||
)}
|
||||
{({ isActive }: { isActive: boolean }) => (
|
||||
<>
|
||||
{isActive ? (
|
||||
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-r-full bg-primary" />
|
||||
) : null}
|
||||
<Icon className="h-5 w-5" />
|
||||
{item.label}
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { getCurrentAuthRedirect } from "@/lib/auth-redirect";
|
||||
import { getSignInHref, getSignInHrefForLocation } from "@/lib/auth-redirect";
|
||||
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
||||
|
||||
type UnauthenticatedErrorCardProps = {
|
||||
@ -7,22 +7,15 @@ type UnauthenticatedErrorCardProps = {
|
||||
onRetry?: () => void;
|
||||
};
|
||||
|
||||
function getSignInHref(redirectTo: string) {
|
||||
return redirectTo === "/"
|
||||
? "/sign-in"
|
||||
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`;
|
||||
}
|
||||
|
||||
export function UnauthenticatedErrorCard({
|
||||
message,
|
||||
onRetry,
|
||||
}: UnauthenticatedErrorCardProps) {
|
||||
const redirectTo =
|
||||
typeof window === "undefined"
|
||||
? "/"
|
||||
: getCurrentAuthRedirect(window.location);
|
||||
const isHostedMode = isHostedClientAuthMode();
|
||||
const signInHref = getSignInHref(redirectTo);
|
||||
const signInHref =
|
||||
typeof window === "undefined"
|
||||
? getSignInHref("/")
|
||||
: getSignInHrefForLocation(window.location);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !isHostedMode) {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
import { normalizeAuthRedirect } from "@/lib/auth-redirect";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
@ -10,19 +8,10 @@ export const authRedirectSearchSchema = z.object({
|
||||
});
|
||||
|
||||
export function useAuthPageState(redirect: string | undefined) {
|
||||
const navigate = useNavigate();
|
||||
const redirectTo = normalizeAuthRedirect(redirect);
|
||||
const { data: session, isPending: isSessionPending } = useSession();
|
||||
const { isPending: isSessionPending } = useSession();
|
||||
const isHostedMode = isHostedClientAuthMode();
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.user?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
void navigate({ href: redirectTo });
|
||||
}, [navigate, redirectTo, session?.user?.id]);
|
||||
|
||||
return {
|
||||
redirectTo,
|
||||
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[]) {
|
||||
return typeof errors[0] === "string" ? errors[0] : null;
|
||||
}
|
||||
|
||||
@ -1,30 +1,132 @@
|
||||
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 {
|
||||
AlertTriangle,
|
||||
ChevronsUpDown,
|
||||
ExternalLink,
|
||||
Menu,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { Sidebar } from "@/client/components/Sidebar";
|
||||
import {
|
||||
dataforseoHelpLinkOptions,
|
||||
getProjectNavItems,
|
||||
} from "@/client/navigation/items";
|
||||
import { getCurrentAuthRedirect } from "@/lib/auth-redirect";
|
||||
AppContent,
|
||||
MissingSeoSetupModal,
|
||||
SeoApiStatusBanners,
|
||||
} from "@/client/layout/AppShellParts";
|
||||
import { getSignInHrefForLocation } from "@/lib/auth-redirect";
|
||||
import { authClient, useSession } from "@/lib/auth-client";
|
||||
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,
|
||||
projectId,
|
||||
pathname,
|
||||
onOpenDrawer,
|
||||
}: {
|
||||
drawerOpen: boolean;
|
||||
projectId: string | null;
|
||||
pathname: string;
|
||||
onOpenDrawer: () => void;
|
||||
}) {
|
||||
const isHostedMode = isHostedClientAuthMode();
|
||||
@ -33,6 +135,7 @@ export function TopNav({
|
||||
return (
|
||||
<div className="navbar bg-base-100 border-b border-base-300 shrink-0 gap-2">
|
||||
<div className="flex-none flex items-center md:hidden">
|
||||
{projectId ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-square btn-ghost"
|
||||
@ -42,6 +145,7 @@ export function TopNav({
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
) : null}
|
||||
<span className="font-semibold text-base-content ml-1">OpenSEO</span>
|
||||
</div>
|
||||
|
||||
@ -51,17 +155,17 @@ export function TopNav({
|
||||
</span>
|
||||
{projectId
|
||||
? projectNavItems.map((item) => {
|
||||
const { icon: Icon, matchSegment, ...linkProps } = item;
|
||||
const isActive = pathname.includes(matchSegment);
|
||||
const { icon: Icon, ...linkProps } = item;
|
||||
return (
|
||||
<Link
|
||||
key={linkProps.to}
|
||||
{...linkProps}
|
||||
className={`btn btn-sm gap-2 ${
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium border-transparent"
|
||||
: "btn-ghost text-base-content/60 hover:text-base-content"
|
||||
}`}
|
||||
activeOptions={{ exact: false, includeSearch: false }}
|
||||
className="btn btn-sm gap-2 btn-ghost text-base-content/60 hover:text-base-content"
|
||||
activeProps={{
|
||||
className:
|
||||
"bg-primary/10 text-primary font-medium border-transparent hover:text-primary",
|
||||
}}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
@ -117,15 +221,11 @@ function HostedSessionActions({
|
||||
}
|
||||
|
||||
const handleSignOut = () => {
|
||||
const redirectTo = getCurrentAuthRedirect(window.location);
|
||||
const signInHref = getSignInHrefForLocation(window.location);
|
||||
void authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
window.location.assign(
|
||||
redirectTo === "/"
|
||||
? "/sign-in"
|
||||
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`,
|
||||
);
|
||||
window.location.assign(signInHref);
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -165,162 +265,3 @@ function HostedSessionActions({
|
||||
</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;
|
||||
}
|
||||
|
||||
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;
|
||||
search: string;
|
||||
hash?: string;
|
||||
}) {
|
||||
return normalizeAuthRedirect(
|
||||
return getSignInHref(
|
||||
normalizeAuthRedirect(
|
||||
`${location.pathname}${location.search}${location.hash ?? ""}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,36 +10,41 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as HelpDataforseoApiKeyRouteImport } from './routes/help/dataforseo-api-key'
|
||||
import { Route as ProjectRouteRouteImport } from './routes/_project/route'
|
||||
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 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 PProjectIdAuditIndexRouteImport } from './routes/p/$projectId/audit/index'
|
||||
import { Route as PProjectIdAuditIssuesResultIdRouteImport } from './routes/p/$projectId/audit/issues/$resultId'
|
||||
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
||||
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({
|
||||
id: '/_auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
const ProjectRouteRoute = ProjectRouteRouteImport.update({
|
||||
id: '/_project',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const HelpDataforseoApiKeyRoute = HelpDataforseoApiKeyRouteImport.update({
|
||||
id: '/help/dataforseo-api-key',
|
||||
path: '/help/dataforseo-api-key',
|
||||
const AppRouteRoute = AppRouteRouteImport.update({
|
||||
id: '/_app',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AppIndexRoute = AppIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} as any)
|
||||
const AuthSignUpRoute = AuthSignUpRouteImport.update({
|
||||
id: '/sign-up',
|
||||
path: '/sign-up',
|
||||
@ -50,121 +55,131 @@ const AuthSignInRoute = AuthSignInRouteImport.update({
|
||||
path: '/sign-in',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} 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({
|
||||
id: '/api/auth/$',
|
||||
path: '/api/auth/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} 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: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => PProjectIdAuditRoute,
|
||||
getParentRoute: () => ProjectPProjectIdRouteRoute,
|
||||
} as any)
|
||||
const PProjectIdAuditIssuesResultIdRoute =
|
||||
PProjectIdAuditIssuesResultIdRouteImport.update({
|
||||
const ProjectPProjectIdSavedRoute = ProjectPProjectIdSavedRouteImport.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',
|
||||
path: '/issues/$resultId',
|
||||
getParentRoute: () => PProjectIdAuditRoute,
|
||||
getParentRoute: () => ProjectPProjectIdAuditRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/p/$projectId': typeof PProjectIdRouteRouteWithChildren
|
||||
'/': typeof AppIndexRoute
|
||||
'/sign-in': typeof AuthSignInRoute
|
||||
'/sign-up': typeof AuthSignUpRoute
|
||||
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute
|
||||
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/p/$projectId/ai': typeof PProjectIdAiRoute
|
||||
'/p/$projectId/audit': typeof PProjectIdAuditRouteWithChildren
|
||||
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute
|
||||
'/p/$projectId/domain': typeof PProjectIdDomainRoute
|
||||
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute
|
||||
'/p/$projectId/saved': typeof PProjectIdSavedRoute
|
||||
'/p/$projectId/': typeof PProjectIdIndexRoute
|
||||
'/p/$projectId/audit/': typeof PProjectIdAuditIndexRoute
|
||||
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute
|
||||
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
|
||||
'/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
|
||||
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
||||
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
||||
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
||||
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
||||
'/p/$projectId/': typeof ProjectPProjectIdIndexRoute
|
||||
'/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
|
||||
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/': typeof AppIndexRoute
|
||||
'/sign-in': typeof AuthSignInRoute
|
||||
'/sign-up': typeof AuthSignUpRoute
|
||||
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute
|
||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/p/$projectId/ai': typeof PProjectIdAiRoute
|
||||
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute
|
||||
'/p/$projectId/domain': typeof PProjectIdDomainRoute
|
||||
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute
|
||||
'/p/$projectId/saved': typeof PProjectIdSavedRoute
|
||||
'/p/$projectId': typeof PProjectIdIndexRoute
|
||||
'/p/$projectId/audit': typeof PProjectIdAuditIndexRoute
|
||||
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute
|
||||
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
|
||||
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
||||
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
||||
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
||||
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
||||
'/p/$projectId': typeof ProjectPProjectIdIndexRoute
|
||||
'/p/$projectId/audit': typeof ProjectPProjectIdAuditIndexRoute
|
||||
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/_app': typeof AppRouteRouteWithChildren
|
||||
'/_project': typeof ProjectRouteRouteWithChildren
|
||||
'/_auth': typeof AuthRouteWithChildren
|
||||
'/p/$projectId': typeof PProjectIdRouteRouteWithChildren
|
||||
'/_auth/sign-in': typeof AuthSignInRoute
|
||||
'/_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
|
||||
'/p/$projectId/ai': typeof PProjectIdAiRoute
|
||||
'/p/$projectId/audit': typeof PProjectIdAuditRouteWithChildren
|
||||
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute
|
||||
'/p/$projectId/domain': typeof PProjectIdDomainRoute
|
||||
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute
|
||||
'/p/$projectId/saved': typeof PProjectIdSavedRoute
|
||||
'/p/$projectId/': typeof PProjectIdIndexRoute
|
||||
'/p/$projectId/audit/': typeof PProjectIdAuditIndexRoute
|
||||
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute
|
||||
'/_project/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
|
||||
'/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
|
||||
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
|
||||
'/_project/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
|
||||
'/_project/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
|
||||
'/_project/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
|
||||
'/_project/p/$projectId/': typeof ProjectPProjectIdIndexRoute
|
||||
'/_project/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
|
||||
'/_project/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/p/$projectId'
|
||||
| '/sign-in'
|
||||
| '/sign-up'
|
||||
| '/p/$projectId'
|
||||
| '/help/dataforseo-api-key'
|
||||
| '/api/auth/$'
|
||||
| '/p/$projectId/ai'
|
||||
@ -193,29 +208,30 @@ export interface FileRouteTypes {
|
||||
| '/p/$projectId/audit/issues/$resultId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_app'
|
||||
| '/_project'
|
||||
| '/_auth'
|
||||
| '/p/$projectId'
|
||||
| '/_auth/sign-in'
|
||||
| '/_auth/sign-up'
|
||||
| '/help/dataforseo-api-key'
|
||||
| '/_app/'
|
||||
| '/_project/p/$projectId'
|
||||
| '/_app/help/dataforseo-api-key'
|
||||
| '/api/auth/$'
|
||||
| '/p/$projectId/ai'
|
||||
| '/p/$projectId/audit'
|
||||
| '/p/$projectId/backlinks'
|
||||
| '/p/$projectId/domain'
|
||||
| '/p/$projectId/keywords'
|
||||
| '/p/$projectId/saved'
|
||||
| '/p/$projectId/'
|
||||
| '/p/$projectId/audit/'
|
||||
| '/p/$projectId/audit/issues/$resultId'
|
||||
| '/_project/p/$projectId/ai'
|
||||
| '/_project/p/$projectId/audit'
|
||||
| '/_project/p/$projectId/backlinks'
|
||||
| '/_project/p/$projectId/domain'
|
||||
| '/_project/p/$projectId/keywords'
|
||||
| '/_project/p/$projectId/saved'
|
||||
| '/_project/p/$projectId/'
|
||||
| '/_project/p/$projectId/audit/'
|
||||
| '/_project/p/$projectId/audit/issues/$resultId'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AppRouteRoute: typeof AppRouteRouteWithChildren
|
||||
ProjectRouteRoute: typeof ProjectRouteRouteWithChildren
|
||||
AuthRoute: typeof AuthRouteWithChildren
|
||||
PProjectIdRouteRoute: typeof PProjectIdRouteRouteWithChildren
|
||||
HelpDataforseoApiKeyRoute: typeof HelpDataforseoApiKeyRoute
|
||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
||||
}
|
||||
|
||||
@ -228,20 +244,27 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
'/_project': {
|
||||
id: '/_project'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
preLoaderRoute: typeof ProjectRouteRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/help/dataforseo-api-key': {
|
||||
id: '/help/dataforseo-api-key'
|
||||
path: '/help/dataforseo-api-key'
|
||||
fullPath: '/help/dataforseo-api-key'
|
||||
preLoaderRoute: typeof HelpDataforseoApiKeyRouteImport
|
||||
'/_app': {
|
||||
id: '/_app'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AppRouteRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_app/': {
|
||||
id: '/_app/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AppIndexRouteImport
|
||||
parentRoute: typeof AppRouteRoute
|
||||
}
|
||||
'/_auth/sign-up': {
|
||||
id: '/_auth/sign-up'
|
||||
path: '/sign-up'
|
||||
@ -256,62 +279,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthSignInRouteImport
|
||||
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/$': {
|
||||
id: '/api/auth/$'
|
||||
path: '/api/auth/$'
|
||||
@ -319,23 +286,155 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ApiAuthSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/p/$projectId/audit/': {
|
||||
id: '/p/$projectId/audit/'
|
||||
'/_app/help/dataforseo-api-key': {
|
||||
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: '/'
|
||||
fullPath: '/p/$projectId/audit/'
|
||||
preLoaderRoute: typeof PProjectIdAuditIndexRouteImport
|
||||
parentRoute: typeof PProjectIdAuditRoute
|
||||
preLoaderRoute: typeof ProjectPProjectIdAuditIndexRouteImport
|
||||
parentRoute: typeof ProjectPProjectIdAuditRoute
|
||||
}
|
||||
'/p/$projectId/audit/issues/$resultId': {
|
||||
id: '/p/$projectId/audit/issues/$resultId'
|
||||
'/_project/p/$projectId/audit/issues/$resultId': {
|
||||
id: '/_project/p/$projectId/audit/issues/$resultId'
|
||||
path: '/issues/$resultId'
|
||||
fullPath: '/p/$projectId/audit/issues/$resultId'
|
||||
preLoaderRoute: typeof PProjectIdAuditIssuesResultIdRouteImport
|
||||
parentRoute: typeof PProjectIdAuditRoute
|
||||
preLoaderRoute: typeof ProjectPProjectIdAuditIssuesResultIdRouteImport
|
||||
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 {
|
||||
AuthSignInRoute: typeof AuthSignInRoute
|
||||
AuthSignUpRoute: typeof AuthSignUpRoute
|
||||
@ -348,49 +447,10 @@ const AuthRouteChildren: 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 = {
|
||||
IndexRoute: IndexRoute,
|
||||
AppRouteRoute: AppRouteRouteWithChildren,
|
||||
ProjectRouteRoute: ProjectRouteRouteWithChildren,
|
||||
AuthRoute: AuthRouteWithChildren,
|
||||
PProjectIdRouteRoute: PProjectIdRouteRouteWithChildren,
|
||||
HelpDataforseoApiKeyRoute: HelpDataforseoApiKeyRoute,
|
||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
|
||||
@ -5,28 +5,16 @@ import {
|
||||
Outlet,
|
||||
Scripts,
|
||||
createRootRoute,
|
||||
useLocation,
|
||||
useMatches,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary";
|
||||
import { NotFound } from "@/client/components/NotFound";
|
||||
import appCss from "@/client/styles/app.css?url";
|
||||
import { Toaster } from "sonner";
|
||||
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({
|
||||
head: () => ({
|
||||
@ -77,112 +65,7 @@ export const Route = createRootRoute({
|
||||
});
|
||||
|
||||
function AppLayout() {
|
||||
const isAuthLayout = useMatches({
|
||||
select: (matches) => matches.some((match) => match.routeId === "/_auth"),
|
||||
});
|
||||
|
||||
if (isAuthLayout) {
|
||||
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 }) {
|
||||
|
||||
@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import { AuthConfigErrorCard } from "@/client/components/AuthConfigErrorCard";
|
||||
import { UnauthenticatedErrorCard } from "@/client/components/UnauthenticatedErrorCard";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
export const Route = createFileRoute("/_app/")({
|
||||
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 {
|
||||
AuthPageCard,
|
||||
authRedirectSearchSchema,
|
||||
getAuthLinkSearch,
|
||||
useAuthPageState,
|
||||
} from "@/client/features/auth/AuthPage";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { getSignInSearch } from "@/lib/auth-redirect";
|
||||
import { z } from "zod";
|
||||
|
||||
const signInSchema = z.object({
|
||||
@ -96,7 +96,7 @@ function SignInPage() {
|
||||
Need an account?{" "}
|
||||
<Link
|
||||
to="/sign-up"
|
||||
search={getAuthLinkSearch(redirectTo)}
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
>
|
||||
Create account
|
||||
|
||||
@ -3,11 +3,11 @@ import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
AuthPageCard,
|
||||
authRedirectSearchSchema,
|
||||
getAuthLinkSearch,
|
||||
getFieldError,
|
||||
useAuthPageState,
|
||||
} from "@/client/features/auth/AuthPage";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { getSignInSearch } from "@/lib/auth-redirect";
|
||||
import {
|
||||
HOSTED_PASSWORD_MAX_LENGTH,
|
||||
HOSTED_PASSWORD_MIN_LENGTH,
|
||||
@ -130,7 +130,7 @@ function SignUpPage() {
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getAuthLinkSearch(redirectTo)}
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
>
|
||||
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")({
|
||||
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 (
|
||||
<div className="min-h-[100dvh] bg-base-200">
|
||||
<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 =
|
||||
"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,
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/p/$projectId/audit")({
|
||||
export const Route = createFileRoute("/_project/p/$projectId/audit")({
|
||||
component: SiteAuditLayout,
|
||||
});
|
||||
|
||||
@ -19,8 +19,8 @@ import {
|
||||
SUPPORT_URL,
|
||||
} from "@/client/features/audit/shared";
|
||||
|
||||
export const Route = createFileRoute<"/p/$projectId/audit/">(
|
||||
"/p/$projectId/audit/",
|
||||
export const Route = createFileRoute<"/_project/p/$projectId/audit/">(
|
||||
"/_project/p/$projectId/audit/",
|
||||
)({
|
||||
validateSearch: auditSearchSchema,
|
||||
component: SiteAuditPage,
|
||||
@ -2,7 +2,9 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { PsiIssuesScreen } from "@/client/features/psi/issues/PsiIssuesScreen";
|
||||
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,
|
||||
component: AuditIssuesPage,
|
||||
});
|
||||
@ -3,7 +3,7 @@ import { BacklinksPage } from "@/client/features/backlinks/BacklinksPage";
|
||||
import { inferBacklinksSearchScopeFromTarget } from "@/client/features/backlinks/backlinksSearchScope";
|
||||
import { backlinksSearchSchema } from "@/types/schemas/backlinks";
|
||||
|
||||
export const Route = createFileRoute("/p/$projectId/backlinks")({
|
||||
export const Route = createFileRoute("/_project/p/$projectId/backlinks")({
|
||||
validateSearch: backlinksSearchSchema,
|
||||
component: BacklinksRoute,
|
||||
});
|
||||
@ -7,7 +7,7 @@ import {
|
||||
} from "@/client/features/domain/utils";
|
||||
import { domainSearchSchema } from "@/types/schemas/domain";
|
||||
|
||||
export const Route = createFileRoute("/p/$projectId/domain")({
|
||||
export const Route = createFileRoute("/_project/p/$projectId/domain")({
|
||||
validateSearch: domainSearchSchema,
|
||||
component: DomainOverviewRoute,
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/p/$projectId/")({
|
||||
export const Route = createFileRoute("/_project/p/$projectId/")({
|
||||
beforeLoad: ({ params }) => {
|
||||
throw redirect({
|
||||
to: "/p/$projectId/keywords",
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from "@/client/features/keywords/keywordSearchParams";
|
||||
import { keywordsSearchSchema } from "@/types/schemas/keywords";
|
||||
|
||||
export const Route = createFileRoute("/p/$projectId/keywords")({
|
||||
export const Route = createFileRoute("/_project/p/$projectId/keywords")({
|
||||
validateSearch: keywordsSearchSchema,
|
||||
beforeLoad: ({ params, 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 { getStandardErrorMessage } from "@/client/lib/error-messages";
|
||||
|
||||
export const Route = createFileRoute("/p/$projectId/saved")({
|
||||
export const Route = createFileRoute("/_project/p/$projectId/saved")({
|
||||
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