feature/frontend-login #10
@@ -2,6 +2,7 @@ import { Text } from '@radix-ui/themes';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { useLocation, useNavigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { SearchParamKeys } from '../lib/constants';
|
||||||
|
|
||||||
export enum ResponseErrorToastId {
|
export enum ResponseErrorToastId {
|
||||||
NetworkError = 'network-error',
|
NetworkError = 'network-error',
|
||||||
@@ -74,8 +75,8 @@ const defaultResponseErrorHandler =
|
|||||||
// store current path for redirect after login
|
// store current path for redirect after login
|
||||||
const currentPath = location.pathname + location.search;
|
const currentPath = location.pathname + location.search;
|
||||||
const searchParam = new URLSearchParams();
|
const searchParam = new URLSearchParams();
|
||||||
searchParam.set('redirect', currentPath);
|
searchParam.set(SearchParamKeys.Redirect, currentPath);
|
||||||
searchParam.set('message', 'Session expired, please log in again');
|
searchParam.set(SearchParamKeys.Message, 'Session expired, please log in again');
|
||||||
navigate(`/login?${searchParam.toString()}`);
|
navigate(`/login?${searchParam.toString()}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
4
apps/frontend/app/lib/constants.ts
Normal file
4
apps/frontend/app/lib/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum SearchParamKeys {
|
||||||
|
Redirect = 'redirect',
|
||||||
|
Message = 'message',
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Box, Container, Flex, Heading } from '@radix-ui/themes';
|
import { Box, Container, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { Slide, toast, ToastContainer } from 'react-toastify';
|
import { Slide, toast, ToastContainer } from 'react-toastify';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
import { useResponseErrorHandler } from '../../hooks/ResponseHelper';
|
import { useResponseErrorHandler } from '../../hooks/ResponseHelper';
|
||||||
import { useApi } from '../../providers/ApiProvider';
|
import { useApi } from '../../providers/ApiProvider';
|
||||||
import { formHook } from '../../providers/FormProvider';
|
import { formHook } from '../../providers/FormProvider';
|
||||||
import type { Route } from './+types/login';
|
import type { Route } from './+types/login';
|
||||||
|
import { SearchParamKeys } from '../../lib/constants';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const loginFormSchema = v.object({
|
const loginFormSchema = v.object({
|
||||||
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
username: v.pipe(v.string(), v.trim(), v.minLength(1, 'Username is required')),
|
||||||
@@ -18,14 +20,23 @@ export function meta({}: Route.MetaArgs): Route.MetaDescriptors {
|
|||||||
return [{ title: 'Login | YANPM' }];
|
return [{ title: 'Login | YANPM' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remember me
|
||||||
export default function LoginRoute() {
|
export default function LoginRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { tanstackApiClient } = useApi();
|
const { tanstackApiClient } = useApi();
|
||||||
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
const { defaultResponseErrorHandler } = useResponseErrorHandler();
|
||||||
|
const [previousSearchParamMessage, setPreviousSearchParamMessage] = useState<string>('');
|
||||||
|
|
||||||
const { mutateAsync: login, isPending } = useMutation({
|
const { mutateAsync: login, isPending } = useMutation({
|
||||||
...tanstackApiClient.mutation('post', '/api/auth/login').mutationOptions,
|
...tanstackApiClient.mutation('post', '/api/auth/login').mutationOptions,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const redirectTo = searchParams.get(SearchParamKeys.Redirect);
|
||||||
|
if (redirectTo) {
|
||||||
|
navigate(redirectTo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigate('/');
|
navigate('/');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -50,6 +61,25 @@ export default function LoginRoute() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const message = searchParams.get(SearchParamKeys.Message);
|
||||||
|
if (message && message !== previousSearchParamMessage) {
|
||||||
|
setPreviousSearchParamMessage(message);
|
||||||
|
toast.info(message, {
|
||||||
|
position: 'top-center',
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'colored',
|
||||||
|
toastId: 'login-route-info-message',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex align="center" justify="center" style={{ minHeight: 'calc(100vh - 64px)' }}>
|
<Flex align="center" justify="center" style={{ minHeight: 'calc(100vh - 64px)' }}>
|
||||||
@@ -110,7 +140,7 @@ export default function LoginRoute() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ marginTop: 18, display: 'flex', gap: 8 }}>
|
<div style={{ marginTop: 18, display: 'flex', gap: 8, justifySelf: 'center' }}>
|
||||||
<form.SubmitButton
|
<form.SubmitButton
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
label={{
|
label={{
|
||||||
|
|||||||
Reference in New Issue
Block a user