feat: implement complex selection flow
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -2,7 +2,9 @@ import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||
import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler';
|
||||
import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler';
|
||||
import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler';
|
||||
import { getCurrentComplexHandler } from '@/modules/complex/handlers/get-current-complex.handler';
|
||||
import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler';
|
||||
import { selectComplexHandler } from '@/modules/complex/handlers/select-complex.handler';
|
||||
import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler';
|
||||
import type { AppEnv } from '@/types/hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
@@ -17,6 +19,8 @@ const complexSlugParamsSchema = z.object({ slug: z.string().trim().min(1) });
|
||||
complexRoutes.use('*', requireAuth);
|
||||
|
||||
complexRoutes.post('/', zValidator('json', createComplexSchema), createComplexHandler);
|
||||
complexRoutes.get('/me', getCurrentComplexHandler);
|
||||
complexRoutes.post('/select', selectComplexHandler);
|
||||
complexRoutes.get('/mine', listMyComplexesHandler);
|
||||
complexRoutes.get(
|
||||
'/slug/:slug',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getCurrentComplex } from '@/modules/complex/services/complex.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { getCookie } from 'hono/cookie';
|
||||
|
||||
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||
|
||||
export async function getCurrentComplexHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
|
||||
|
||||
if (!complexId) {
|
||||
return c.json(null);
|
||||
}
|
||||
|
||||
const complex = await getCurrentComplex(user.id, complexId);
|
||||
|
||||
if (!complex) {
|
||||
return c.json(null);
|
||||
}
|
||||
|
||||
return c.json(complex);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { selectComplex } from '@/modules/complex/services/complex.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { setCookie } from 'hono/cookie';
|
||||
import { z } from 'zod';
|
||||
|
||||
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
||||
|
||||
const selectComplexSchema = z.object({
|
||||
complexId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export async function selectComplexHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const body = await c.req.json();
|
||||
const result = selectComplexSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
return c.json({ message: 'Invalid payload', issues: result.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const { complexId } = result.data;
|
||||
const complex = await selectComplex(user.id, complexId);
|
||||
|
||||
if (!complex) {
|
||||
return c.json({ message: 'Complex not found or not associated with user' }, { status: 404 });
|
||||
}
|
||||
|
||||
setCookie(c, SELECTED_COMPLEX_COOKIE, complexId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
});
|
||||
|
||||
return c.json(complex);
|
||||
}
|
||||
@@ -104,7 +104,52 @@ export async function listMyComplexes(userId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
return complexUsers.map((complexUser) => complexUser.complex);
|
||||
return complexUsers.map((complexUser) => ({
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getCurrentComplex(userId: string, complexId: string) {
|
||||
const complexUser = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
complex: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!complexUser) return null;
|
||||
|
||||
return {
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
};
|
||||
}
|
||||
|
||||
export async function selectComplex(userId: string, complexId: string) {
|
||||
const complexUser = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
complex: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!complexUser) return null;
|
||||
|
||||
return {
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateComplex(id: string, input: UpdateComplexInput) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { getUserProfile } from '@/modules/user/services/user.service';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { getCookie } from 'hono/cookie';
|
||||
|
||||
export function getUserProfileHandler(c: AppContext) {
|
||||
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||
|
||||
export async function getUserProfileHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const profile = getUserProfile(user);
|
||||
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
|
||||
const profile = await getUserProfile(user, complexId);
|
||||
return c.json(profile);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { User } from '@/lib/auth';
|
||||
import { db } from '@/lib/prisma';
|
||||
import type { UserProfile } from '@repo/api-contract';
|
||||
|
||||
function getGravatarUrl(email: string): string {
|
||||
@@ -7,12 +8,29 @@ function getGravatarUrl(email: string): string {
|
||||
return `https://www.gravatar.com/avatar/${hash}?d=identicon`;
|
||||
}
|
||||
|
||||
export function getUserProfile(user: User): UserProfile {
|
||||
export async function getUserProfile(user: User, complexId?: string): Promise<UserProfile> {
|
||||
let role = 'member';
|
||||
|
||||
if (complexId) {
|
||||
const complexUser = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (complexUser) {
|
||||
role = complexUser.role.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
fullName: user.name,
|
||||
email: user.email,
|
||||
role: (user as any).role || 'member',
|
||||
role,
|
||||
avatarUrl: user.image || getGravatarUrl(user.email),
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
};
|
||||
|
||||
@@ -20,13 +20,10 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ApiClientError, apiClient } from '@/lib/api-client';
|
||||
import {
|
||||
getCurrentComplexSlug,
|
||||
setCurrentComplexSlug as persistCurrentComplexSlug,
|
||||
} from '@/lib/current-complex';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import type { AdminBooking } from '@repo/api-contract';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -123,9 +120,9 @@ function effectiveStatusClassName(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED
|
||||
|
||||
export function HomePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const todayIso = useMemo(() => toIsoDateLocal(new Date()), []);
|
||||
|
||||
const [currentComplexSlug, setCurrentComplexSlug] = useState<string | null>(null);
|
||||
const [fromDate, setFromDate] = useState(todayIso);
|
||||
const [manualDate, setManualDate] = useState(todayIso);
|
||||
const [selectedSportId, setSelectedSportId] = useState<string | undefined>();
|
||||
@@ -134,35 +131,32 @@ export function HomePage() {
|
||||
const [isManualBookingOpen, setIsManualBookingOpen] = useState(false);
|
||||
const [urlCopied, setUrlCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentComplexSlug(getCurrentComplexSlug());
|
||||
}, []);
|
||||
const currentComplexQuery = useQuery({
|
||||
queryKey: ['current-complex'],
|
||||
queryFn: () => apiClient.complexes.getCurrent(),
|
||||
});
|
||||
|
||||
const myComplexesQuery = useQuery({
|
||||
queryKey: ['my-complexes'],
|
||||
queryFn: () => apiClient.complexes.listMine(),
|
||||
});
|
||||
|
||||
const selectedComplex = useMemo(() => {
|
||||
const complexes = myComplexesQuery.data ?? [];
|
||||
|
||||
if (complexes.length === 0) return null;
|
||||
|
||||
if (currentComplexSlug) {
|
||||
const match = complexes.find((complex) => complex.complexSlug === currentComplexSlug);
|
||||
|
||||
if (match) return match;
|
||||
useEffect(() => {
|
||||
async function autoSelect() {
|
||||
if (myComplexesQuery.data && myComplexesQuery.data.length === 1) {
|
||||
await apiClient.complexes.select({ complexId: myComplexesQuery.data[0].id });
|
||||
queryClient.invalidateQueries({ queryKey: ['current-complex'] });
|
||||
} else if (myComplexesQuery.data && myComplexesQuery.data.length > 1 && !currentComplexQuery.data) {
|
||||
navigate({ to: '/select-complex' });
|
||||
}
|
||||
}
|
||||
|
||||
return complexes[0];
|
||||
}, [currentComplexSlug, myComplexesQuery.data]);
|
||||
if (!currentComplexQuery.isLoading && !currentComplexQuery.data && myComplexesQuery.data) {
|
||||
autoSelect();
|
||||
}
|
||||
}, [currentComplexQuery.isLoading, currentComplexQuery.data, myComplexesQuery.data, navigate, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedComplex) return;
|
||||
|
||||
setCurrentComplexSlug(selectedComplex.complexSlug);
|
||||
persistCurrentComplexSlug(selectedComplex.complexSlug);
|
||||
}, [selectedComplex]);
|
||||
const selectedComplex = currentComplexQuery.data ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedComplex?.id) return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { authClient } from '@/lib/api-client';
|
||||
import { apiClient, authClient } from '@/lib/api-client';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
@@ -38,12 +38,30 @@ export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
|
||||
},
|
||||
});
|
||||
|
||||
async function handlePostLogin() {
|
||||
const complexes = await apiClient.complexes.listMine();
|
||||
|
||||
if (complexes.length === 1) {
|
||||
await apiClient.complexes.select({ complexId: complexes[0].id });
|
||||
navigate({ to: redirectTo });
|
||||
} else if (complexes.length > 1) {
|
||||
const current = await apiClient.complexes.getCurrent();
|
||||
if (current) {
|
||||
navigate({ to: redirectTo });
|
||||
} else {
|
||||
navigate({ to: '/select-complex' });
|
||||
}
|
||||
} else {
|
||||
await navigate({ to: redirectTo });
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (values: LoginForm) => {
|
||||
setSubmitError(null);
|
||||
|
||||
try {
|
||||
await signInWithPassword(values);
|
||||
await navigate({ to: redirectTo });
|
||||
await handlePostLogin();
|
||||
} catch {
|
||||
setSubmitError('No pudimos iniciar sesión. Verificá tus credenciales.');
|
||||
}
|
||||
@@ -55,9 +73,7 @@ export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
|
||||
try {
|
||||
await authClient.signIn.social({
|
||||
provider: 'google',
|
||||
callbackURL: redirectTo.startsWith('http')
|
||||
? redirectTo
|
||||
: `${window.location.origin}${redirectTo}`,
|
||||
callbackURL: `${window.location.origin}/auth-callback`,
|
||||
});
|
||||
} catch {
|
||||
setSubmitError('No pudimos iniciar sesión con Google.');
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import type { ComplexWithRole } from '@repo/api-contract';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Building2, Loader2 } from 'lucide-react';
|
||||
|
||||
export function SelectComplexPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const complexesQuery = useQuery({
|
||||
queryKey: ['my-complexes'],
|
||||
queryFn: () => apiClient.complexes.listMine(),
|
||||
});
|
||||
|
||||
const selectMutation = useMutation({
|
||||
mutationFn: async (complexId: string) => {
|
||||
const response = await apiClient.complexes.select({ complexId });
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate({ to: '/' });
|
||||
},
|
||||
});
|
||||
|
||||
const handleSelect = (complex: ComplexWithRole) => {
|
||||
selectMutation.mutate(complex.id);
|
||||
};
|
||||
|
||||
if (complexesQuery.isLoading) {
|
||||
return (
|
||||
<main className="flex min-h-screen w-full items-center justify-center px-6">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (complexesQuery.isError) {
|
||||
return (
|
||||
<main className="flex min-h-screen w-full items-center justify-center px-6">
|
||||
<div className="text-center">
|
||||
<p className="text-destructive">Error al cargar tus complejos.</p>
|
||||
<Button variant="link" className="mt-2" onClick={() => complexesQuery.refetch()}>
|
||||
Reintentar
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const complexes = complexesQuery.data ?? [];
|
||||
|
||||
if (complexes.length === 0) {
|
||||
return (
|
||||
<main className="flex min-h-screen w-full items-center justify-center px-6">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">No tenés complejos asociados.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen w-full items-center justify-center px-6">
|
||||
<section className="w-full max-w-md space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold">Seleccioná un complejo</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Elegí el complejo con el que vas a trabajar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{complexes.map((complex) => (
|
||||
<button
|
||||
key={complex.id}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-accent"
|
||||
onClick={() => handleSelect(complex)}
|
||||
disabled={selectMutation.isPending}
|
||||
>
|
||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{complex.complexName}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{complex.city ?? 'Sin ciudad'} · {complex.role}
|
||||
</p>
|
||||
</div>
|
||||
{selectMutation.isPending && selectMutation.variables === complex.id && (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AdminBooking,
|
||||
Complex,
|
||||
ComplexWithRole,
|
||||
Court,
|
||||
CreateAdminBookingInput,
|
||||
CreateComplexPayload,
|
||||
@@ -20,6 +21,7 @@ import type {
|
||||
PublicAvailabilityResponse,
|
||||
PublicBooking,
|
||||
PublicBookingConfirmation,
|
||||
SelectComplexInput,
|
||||
Sport,
|
||||
UpdateAdminBookingStatusInput,
|
||||
UpdateComplexPayload,
|
||||
@@ -187,7 +189,17 @@ export const apiClient = {
|
||||
return response.data;
|
||||
},
|
||||
listMine: async () => {
|
||||
const response = await http.get<Complex[]>('/api/complexes/mine');
|
||||
const response = await http.get<ComplexWithRole[]>('/api/complexes/mine');
|
||||
return response.data;
|
||||
},
|
||||
getCurrent: async () => {
|
||||
const response = await http.get<ComplexWithRole | null>('/api/complexes/me');
|
||||
return response.data;
|
||||
},
|
||||
select: async (payload: SelectComplexInput) => {
|
||||
const response = await http.post<ComplexWithRole>('/api/complexes/select', JSON.stringify(payload), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SelectComplexRouteImport } from './routes/select-complex'
|
||||
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
|
||||
import { Route as OnboardRouteImport } from './routes/onboard'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthCallbackRouteImport } from './routes/auth-callback'
|
||||
import { Route as AppRouteRouteImport } from './routes/_app/route'
|
||||
import { Route as OnboardIndexRouteImport } from './routes/onboard/index'
|
||||
import { Route as OnboardVerifyRouteImport } from './routes/onboard/verify'
|
||||
@@ -25,6 +27,11 @@ import { Route as ComplexSlugBookingIndexRouteImport } from './routes/$complexSl
|
||||
import { Route as ComplexSlugBookingConfirmedBookingCodeRouteImport } from './routes/$complexSlug/booking/confirmed/$bookingCode'
|
||||
import { Route as AppAuthenticatedComplexSlugEditRouteImport } from './routes/_app/_authenticated/complex/$slug/edit'
|
||||
|
||||
const SelectComplexRoute = SelectComplexRouteImport.update({
|
||||
id: '/select-complex',
|
||||
path: '/select-complex',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ResetPasswordRoute = ResetPasswordRouteImport.update({
|
||||
id: '/reset-password',
|
||||
path: '/reset-password',
|
||||
@@ -40,6 +47,11 @@ const LoginRoute = LoginRouteImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthCallbackRoute = AuthCallbackRouteImport.update({
|
||||
id: '/auth-callback',
|
||||
path: '/auth-callback',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AppRouteRoute = AppRouteRouteImport.update({
|
||||
id: '/_app',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
@@ -103,9 +115,11 @@ const AppAuthenticatedComplexSlugEditRoute =
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof AppAuthenticatedIndexRoute
|
||||
'/auth-callback': typeof AuthCallbackRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/onboard': typeof OnboardRouteWithChildren
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/select-complex': typeof SelectComplexRoute
|
||||
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
|
||||
'/about': typeof AppAboutRoute
|
||||
'/profile': typeof AppProfileRoute
|
||||
@@ -118,8 +132,10 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof AppAuthenticatedIndexRoute
|
||||
'/auth-callback': typeof AuthCallbackRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/select-complex': typeof SelectComplexRoute
|
||||
'/about': typeof AppAboutRoute
|
||||
'/profile': typeof AppProfileRoute
|
||||
'/onboard/complete': typeof OnboardCompleteRoute
|
||||
@@ -132,9 +148,11 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_app': typeof AppRouteRouteWithChildren
|
||||
'/auth-callback': typeof AuthCallbackRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/onboard': typeof OnboardRouteWithChildren
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
'/select-complex': typeof SelectComplexRoute
|
||||
'/_app/_authenticated': typeof AppAuthenticatedRouteRouteWithChildren
|
||||
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
|
||||
'/_app/about': typeof AppAboutRoute
|
||||
@@ -151,9 +169,11 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/auth-callback'
|
||||
| '/login'
|
||||
| '/onboard'
|
||||
| '/reset-password'
|
||||
| '/select-complex'
|
||||
| '/$complexSlug/booking'
|
||||
| '/about'
|
||||
| '/profile'
|
||||
@@ -166,8 +186,10 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/auth-callback'
|
||||
| '/login'
|
||||
| '/reset-password'
|
||||
| '/select-complex'
|
||||
| '/about'
|
||||
| '/profile'
|
||||
| '/onboard/complete'
|
||||
@@ -179,9 +201,11 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_app'
|
||||
| '/auth-callback'
|
||||
| '/login'
|
||||
| '/onboard'
|
||||
| '/reset-password'
|
||||
| '/select-complex'
|
||||
| '/_app/_authenticated'
|
||||
| '/$complexSlug/booking'
|
||||
| '/_app/about'
|
||||
@@ -197,14 +221,23 @@ export interface FileRouteTypes {
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
AppRouteRoute: typeof AppRouteRouteWithChildren
|
||||
AuthCallbackRoute: typeof AuthCallbackRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
OnboardRoute: typeof OnboardRouteWithChildren
|
||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||
SelectComplexRoute: typeof SelectComplexRoute
|
||||
ComplexSlugBookingRoute: typeof ComplexSlugBookingRouteWithChildren
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/select-complex': {
|
||||
id: '/select-complex'
|
||||
path: '/select-complex'
|
||||
fullPath: '/select-complex'
|
||||
preLoaderRoute: typeof SelectComplexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/reset-password': {
|
||||
id: '/reset-password'
|
||||
path: '/reset-password'
|
||||
@@ -226,6 +259,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/auth-callback': {
|
||||
id: '/auth-callback'
|
||||
path: '/auth-callback'
|
||||
fullPath: '/auth-callback'
|
||||
preLoaderRoute: typeof AuthCallbackRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_app': {
|
||||
id: '/_app'
|
||||
path: ''
|
||||
@@ -375,9 +415,11 @@ const ComplexSlugBookingRouteWithChildren =
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AppRouteRoute: AppRouteRouteWithChildren,
|
||||
AuthCallbackRoute: AuthCallbackRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
OnboardRoute: OnboardRouteWithChildren,
|
||||
ResetPasswordRoute: ResetPasswordRoute,
|
||||
SelectComplexRoute: SelectComplexRoute,
|
||||
ComplexSlugBookingRoute: ComplexSlugBookingRouteWithChildren,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
|
||||
50
apps/frontend/src/routes/auth-callback.tsx
Normal file
50
apps/frontend/src/routes/auth-callback.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useEffect } from 'react';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
|
||||
function AuthCallbackPage() {
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
async function handleCallback() {
|
||||
if (!isAuthenticated) {
|
||||
navigate({ to: '/login' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const complexes = await apiClient.complexes.listMine();
|
||||
|
||||
if (complexes.length === 1) {
|
||||
await apiClient.complexes.select({ complexId: complexes[0].id });
|
||||
navigate({ to: '/' });
|
||||
} else if (complexes.length > 1) {
|
||||
const current = await apiClient.complexes.getCurrent();
|
||||
if (current) {
|
||||
navigate({ to: '/' });
|
||||
} else {
|
||||
navigate({ to: '/select-complex' });
|
||||
}
|
||||
} else {
|
||||
navigate({ to: '/' });
|
||||
}
|
||||
} catch {
|
||||
navigate({ to: '/login' });
|
||||
}
|
||||
}
|
||||
|
||||
handleCallback();
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen w-full items-center justify-center px-6">
|
||||
<p className="text-sm text-muted-foreground">Completando sesión...</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/auth-callback')({
|
||||
component: AuthCallbackPage,
|
||||
});
|
||||
6
apps/frontend/src/routes/select-complex.tsx
Normal file
6
apps/frontend/src/routes/select-complex.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { SelectComplexPage } from '@/features/select-complex/select-complex-page';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/select-complex')({
|
||||
component: SelectComplexPage,
|
||||
});
|
||||
Reference in New Issue
Block a user