feat(24-02): add auth prompt state, modal, usePublicSetup hook, guard onboarding

- Extend uiStore with showAuthPrompt/openAuthPrompt/closeAuthPrompt state
- Create AuthPromptModal component with sign in/sign up CTAs pointing to /login
- Add usePublicSetup hook to useSetups for anonymous setup viewing via public API
- Rework useOnboardingComplete to accept enabled param (guards auth-gated call)
This commit is contained in:
2026-04-10 10:06:59 +02:00
parent afab8175f9
commit cd85715d05
4 changed files with 81 additions and 2 deletions

View File

@@ -0,0 +1,45 @@
import { Link } from "@tanstack/react-router";
import { useUIStore } from "../stores/uiStore";
export function AuthPromptModal() {
const showAuthPrompt = useUIStore((s) => s.showAuthPrompt);
const closeAuthPrompt = useUIStore((s) => s.closeAuthPrompt);
if (!showAuthPrompt) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/30"
onClick={closeAuthPrompt}
onKeyDown={(e) => {
if (e.key === "Escape") closeAuthPrompt();
}}
/>
<div className="relative bg-white rounded-xl shadow-lg p-6 max-w-sm mx-4 w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Join GearBox
</h3>
<p className="text-sm text-gray-600 mb-6">
To manage your own collection, sign in or sign up.
</p>
<div className="flex flex-col gap-3">
<Link
to="/login"
className="w-full text-center px-4 py-2.5 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors"
onClick={closeAuthPrompt}
>
Sign in
</Link>
<Link
to="/login"
className="w-full text-center px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
onClick={closeAuthPrompt}
>
Create account
</Link>
</div>
</div>
</div>
);
}

View File

@@ -32,6 +32,20 @@ export function useUpdateSetting() {
}); });
} }
export function useOnboardingComplete() { export function useOnboardingComplete(enabled = true) {
return useSetting("onboardingComplete"); return useQuery({
queryKey: ["settings", "onboardingComplete"],
queryFn: async () => {
try {
const result = await apiGet<Setting>(
`/api/settings/onboardingComplete`,
);
return result.value;
} catch (err: any) {
if (err?.status === 404) return null;
throw err;
}
},
enabled,
});
} }

View File

@@ -64,6 +64,16 @@ export function useSetup(setupId: number | null) {
}); });
} }
export function usePublicSetup(setupId: number | null) {
return useQuery({
queryKey: ["setups", setupId, "public"],
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}/public`),
enabled: setupId != null,
retry: (count, error) =>
error instanceof ApiError && error.status === 404 ? false : count < 3,
});
}
export function useCreateSetup() { export function useCreateSetup() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({

View File

@@ -79,6 +79,11 @@ interface UIState {
// Session thread tracking // Session thread tracking
catalogSessionThreadId: number | null; catalogSessionThreadId: number | null;
setCatalogSessionThreadId: (id: number | null) => void; setCatalogSessionThreadId: (id: number | null) => void;
// Auth prompt modal
showAuthPrompt: boolean;
openAuthPrompt: () => void;
closeAuthPrompt: () => void;
} }
export const useUIStore = create<UIState>((set) => ({ export const useUIStore = create<UIState>((set) => ({
@@ -184,4 +189,9 @@ export const useUIStore = create<UIState>((set) => ({
// Session thread tracking // Session thread tracking
catalogSessionThreadId: null, catalogSessionThreadId: null,
setCatalogSessionThreadId: (id) => set({ catalogSessionThreadId: id }), setCatalogSessionThreadId: (id) => set({ catalogSessionThreadId: id }),
// Auth prompt modal
showAuthPrompt: false,
openAuthPrompt: () => set({ showAuthPrompt: true }),
closeAuthPrompt: () => set({ showAuthPrompt: false }),
})); }));