feat(24-02): render-first root layout, guarded write actions, public setup viewing

- Remove authLoading spinner gate — app renders immediately for all visitors
- Expand isPublicRoute to include /, /global-items/*, /setups/*, /users/, /login
- Replace hard window.location.href redirect with soft navigate() after auth resolves
- Remove onboarding loading spinner — pass isAuthenticated as enabled to guard query
- Add AuthPromptModal to root JSX for global availability
- Guard Add to Collection and Add to Thread buttons with isAuthenticated check
- Rework setup detail page to use usePublicSetup for anonymous visitors
- Wrap all write action UI (Add Items, Delete, Public toggle, remove/classify) in isAuthenticated guards
This commit is contained in:
2026-04-10 10:09:41 +02:00
parent 50f9629707
commit 7b0efae0c4
3 changed files with 132 additions and 111 deletions

View File

@@ -12,6 +12,7 @@ import { Toaster } from "sonner";
import "../app.css";
import { AddToCollectionModal } from "../components/AddToCollectionModal";
import { AddToThreadModal } from "../components/AddToThreadModal";
import { AuthPromptModal } from "../components/AuthPromptModal";
import { CatalogSearchOverlay } from "../components/CatalogSearchOverlay";
import { ConfirmDialog } from "../components/ConfirmDialog";
import { ExternalLinkDialog } from "../components/ExternalLinkDialog";
@@ -94,7 +95,7 @@ function RootLayout() {
// Onboarding — only check when authenticated (endpoint requires auth)
const { data: onboardingComplete, isLoading: onboardingLoading } =
useOnboardingComplete();
useOnboardingComplete(isAuthenticated);
const [wizardDismissed, setWizardDismissed] = useState(false);
// Don't show onboarding wizard until user has created an account
@@ -117,40 +118,21 @@ function RootLayout() {
const totalsBarProps = isDashboard ? {} : { linkTo: "/" };
// Show loading while checking auth
if (authLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-gray-600 border-t-transparent rounded-full animate-spin" />
</div>
);
}
// Redirect unauthenticated users to login (server-side OIDC route)
// Allow public routes through without auth
const isPublicRoute =
location.pathname.startsWith("/users/") || location.pathname === "/login";
location.pathname === "/" ||
location.pathname.startsWith("/users/") ||
location.pathname.startsWith("/global-items") ||
location.pathname.startsWith("/setups/") ||
location.pathname === "/login";
// FAB visibility: show on all authenticated, non-public routes
const isSetupsPage = !!matchRoute({ to: "/setups", fuzzy: true });
const showFab = isAuthenticated && !isPublicRoute;
if (!isAuthenticated && !isPublicRoute) {
window.location.href = "/login";
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-sm text-gray-500">Redirecting to login...</p>
</div>
);
}
// Show loading while checking onboarding status
if (onboardingLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-gray-600 border-t-transparent rounded-full animate-spin" />
</div>
);
if (!isAuthenticated && !isPublicRoute && !authLoading) {
navigate({ to: "/login" });
return null;
}
return (
@@ -198,6 +180,9 @@ function RootLayout() {
<AddToThreadModal />
<Toaster position="bottom-right" richColors />
{/* Auth Prompt Modal */}
<AuthPromptModal />
{/* Onboarding Wizard */}
{showWizard && (
<OnboardingWizard onComplete={() => setWizardDismissed(true)} />