Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,25 +1,61 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { isAuthenticatedAtom } from '@/atoms/auth';
|
||||
import { isSupabaseConfigured } from '@/services/supabase';
|
||||
import {
|
||||
backendUserAtom,
|
||||
isAuthenticatedAtom,
|
||||
authInitialCheckDoneAtom,
|
||||
guestSessionAtom,
|
||||
guestSessionHydratedAtom,
|
||||
} from '@/atoms/auth';
|
||||
import { hasSessionToken } from '@/services/backend';
|
||||
import { ROUTES } from '@/utils/constants';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
/** Allow access while name/age are still missing (profile completion page). */
|
||||
allowIncompleteProfile?: boolean;
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const AuthBootSpinner = () => (
|
||||
<div className="flex h-full min-h-0 flex-1 items-center justify-center">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-2 border-neon-cyan/30 border-t-neon-cyan" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export function ProtectedRoute({ children, allowIncompleteProfile = false }: ProtectedRouteProps) {
|
||||
const [isAuthenticated] = useAtom(isAuthenticatedAtom);
|
||||
const [backendUser] = useAtom(backendUserAtom);
|
||||
const [authInitialCheckDone] = useAtom(authInitialCheckDoneAtom);
|
||||
const [guestSession] = useAtom(guestSessionAtom);
|
||||
const [guestSessionHydrated] = useAtom(guestSessionHydratedAtom);
|
||||
const location = useLocation();
|
||||
// Stabilize Navigate `state` — a new object every render re-triggers Navigate's
|
||||
// useEffect (deps include `state`) and can blow the update depth limit.
|
||||
const loginRedirectState = useMemo(
|
||||
() => ({ from: { pathname: location.pathname } }),
|
||||
[location.pathname],
|
||||
);
|
||||
const hasToken = hasSessionToken();
|
||||
const canUseBackendUser = Boolean(backendUser && hasToken);
|
||||
|
||||
// In offline mode, allow access without authentication
|
||||
if (!isSupabaseConfigured) {
|
||||
return <>{children}</>;
|
||||
if (!authInitialCheckDone) {
|
||||
return <AuthBootSpinner />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
if (!guestSessionHydrated) {
|
||||
return <AuthBootSpinner />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
if (!isAuthenticated && !guestSession) {
|
||||
return <Navigate to={ROUTES.LOGIN} state={loginRedirectState} replace />;
|
||||
}
|
||||
|
||||
// Incomplete-profile gate only when a live session exists. Stale backendUser
|
||||
// without a token must not bounce / ↔ /profile forever.
|
||||
if (!allowIncompleteProfile && canUseBackendUser && !backendUser!.profileComplete) {
|
||||
return <Navigate to={ROUTES.PROFILE} replace />;
|
||||
}
|
||||
|
||||
return <div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">{children}</div>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user