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:
ar.badiei
2026-09-03 21:01:52 +03:30
co-authored by Cursor
parent aa9236fb92
commit 370ac15471
193 changed files with 25997 additions and 1991 deletions
+46 -10
View File
@@ -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>;
}