login and logout
This commit is contained in:
parent
4327a33837
commit
b427c148af
2
src/.env
2
src/.env
@ -1 +1 @@
|
|||||||
VITE_API_URL=http://localhost:8000
|
VITE_API_URL=http://127.0.0.1:8000
|
||||||
36
src/App.tsx
36
src/App.tsx
@ -3,24 +3,30 @@ import "./App.css";
|
|||||||
import router from "./router/router";
|
import router from "./router/router";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<AuthProvider>
|
||||||
<ToastContainer
|
<RouterProvider router={router} />
|
||||||
position="top-right"
|
<ToastContainer
|
||||||
autoClose={3000}
|
position="top-right"
|
||||||
hideProgressBar={false}
|
autoClose={3000}
|
||||||
newestOnTop={false}
|
hideProgressBar={false}
|
||||||
closeOnClick
|
newestOnTop={false}
|
||||||
rtl={false}
|
closeOnClick
|
||||||
pauseOnFocusLoss
|
rtl={false}
|
||||||
draggable
|
pauseOnFocusLoss
|
||||||
pauseOnHover
|
draggable
|
||||||
theme="colored"
|
pauseOnHover
|
||||||
/>
|
theme="colored"
|
||||||
</>
|
/>
|
||||||
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/api/authApi.ts
Normal file
19
src/api/authApi.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type {
|
||||||
|
LoginPayload,
|
||||||
|
LoginResponse,
|
||||||
|
MeResponse,
|
||||||
|
User,
|
||||||
|
} from "../components/shared/types/AuthTypes";
|
||||||
|
import axiosInstance from "./axiosInstance";
|
||||||
|
|
||||||
|
export const loginRequest = async (
|
||||||
|
data: LoginPayload
|
||||||
|
): Promise<LoginResponse> => {
|
||||||
|
const res = await axiosInstance.post<LoginResponse>("/api/auth/login", data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMe = async (): Promise<User> => {
|
||||||
|
const res = await axiosInstance.get<MeResponse>("/api/auth/me");
|
||||||
|
return res.data.user;
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: "http://127.0.0.1:8000",
|
||||||
timeout: 30 * 1000,
|
timeout: 30 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,16 @@ import Typography from "@mui/material/Typography";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Container from "../../components/shared/Container";
|
import Container from "../../components/shared/Container";
|
||||||
import StarIcon from '@mui/icons-material/Star';
|
import StarIcon from "@mui/icons-material/Star";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
|
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@ -24,11 +29,13 @@ function Header() {
|
|||||||
setAnchorElNav(null);
|
setAnchorElNav(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goToAdmin = () => navigate("/admin");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar sx={{backgroundColor: "#4b2981"}} position="static">
|
<AppBar sx={{ backgroundColor: "#4b2981" }} position="static">
|
||||||
<Container>
|
<Container>
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
<StarIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
|
<StarIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
noWrap
|
noWrap
|
||||||
@ -61,28 +68,47 @@ function Header() {
|
|||||||
<Menu
|
<Menu
|
||||||
id="menu-appbar"
|
id="menu-appbar"
|
||||||
anchorEl={anchorElNav}
|
anchorEl={anchorElNav}
|
||||||
anchorOrigin={{
|
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
keepMounted
|
keepMounted
|
||||||
transformOrigin={{
|
transformOrigin={{ vertical: "top", horizontal: "left" }}
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
open={Boolean(anchorElNav)}
|
open={Boolean(anchorElNav)}
|
||||||
onClose={handleCloseNavMenu}
|
onClose={handleCloseNavMenu}
|
||||||
sx={{ display: { xs: "block", md: "none" } }}
|
sx={{ display: { xs: "block", md: "none" } }}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleCloseNavMenu} component="a" href="/categories">
|
{!user && (
|
||||||
<Typography textAlign="center">Categories</Typography>
|
<MenuItem
|
||||||
</MenuItem>
|
onClick={() => {
|
||||||
<MenuItem onClick={handleCloseNavMenu} component="a" href="/profile">
|
handleCloseNavMenu();
|
||||||
<Typography textAlign="center">Profile</Typography>
|
navigate("/login");
|
||||||
</MenuItem>
|
}}
|
||||||
<MenuItem onClick={handleCloseNavMenu} component="a" href="/create">
|
>
|
||||||
<Typography textAlign="center">Create</Typography>
|
<Typography textAlign="center">Login</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
|
<span>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleCloseNavMenu();
|
||||||
|
navigate("/profile");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography textAlign="center">Profile</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
{user.type === "admin" && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleCloseNavMenu();
|
||||||
|
goToAdmin();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography textAlign="center">
|
||||||
|
Admin Dashboard
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -91,7 +117,7 @@ function Header() {
|
|||||||
variant="h5"
|
variant="h5"
|
||||||
noWrap
|
noWrap
|
||||||
component="a"
|
component="a"
|
||||||
href="#home"
|
href="/"
|
||||||
sx={{
|
sx={{
|
||||||
mr: 2,
|
mr: 2,
|
||||||
display: { xs: "flex", md: "none" },
|
display: { xs: "flex", md: "none" },
|
||||||
@ -107,15 +133,33 @@ function Header() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}>
|
<Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}>
|
||||||
<Button href="/categories" sx={{ my: 2, color: "white", display: "block" }}>
|
{!user && (
|
||||||
Categories
|
<Button
|
||||||
</Button>
|
onClick={() => navigate("/login")}
|
||||||
<Button href="/profile" sx={{ my: 2, color: "white", display: "block" }}>
|
sx={{ my: 2, color: "white", display: "block" }}
|
||||||
Profile
|
>
|
||||||
</Button>
|
Login
|
||||||
<Button href="/create" sx={{ my: 2, color: "white", display: "block" }}>
|
</Button>
|
||||||
Create
|
)}
|
||||||
</Button>
|
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/profile")}
|
||||||
|
sx={{ my: 2, color: "white", display: "block" }}
|
||||||
|
>
|
||||||
|
Profile
|
||||||
|
</Button>
|
||||||
|
{user.type === "admin" && (
|
||||||
|
<Button
|
||||||
|
onClick={goToAdmin}
|
||||||
|
sx={{ my: 2, color: "white", display: "block" }}
|
||||||
|
>
|
||||||
|
Admin Dashboard
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
18
src/components/ProtectedRoute/ProtectedRoute.tsx
Normal file
18
src/components/ProtectedRoute/ProtectedRoute.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
import Unauthorized from "../../pages/Unauthorized/Unauthorized";
|
||||||
|
import Container from "../shared/Container";
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||||
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
|
if (isLoading) return <Container>Loading...</Container>;
|
||||||
|
|
||||||
|
if (!user) return <Unauthorized />;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@ -40,7 +40,7 @@ const Question = ({ question }: QuestionProps) => {
|
|||||||
Difficulty: {question.difficulty}
|
Difficulty: {question.difficulty}
|
||||||
</Typography>
|
</Typography>
|
||||||
<AuthorMeta>
|
<AuthorMeta>
|
||||||
<PersonIcon/>
|
<PersonIcon />
|
||||||
<Typography variant="body1">{question.author}</Typography>
|
<Typography variant="body1">{question.author}</Typography>
|
||||||
</AuthorMeta>
|
</AuthorMeta>
|
||||||
</QuestionMetadata>
|
</QuestionMetadata>
|
||||||
|
|||||||
24
src/components/shared/types/AuthTypes.ts
Normal file
24
src/components/shared/types/AuthTypes.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
email_verified_at: string | null;
|
||||||
|
type: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginPayload {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeResponse {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
55
src/context/AuthContext.tsx
Normal file
55
src/context/AuthContext.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useCurrentUser } from "../hooks/useCurrentUser";
|
||||||
|
import { useLogin } from "../hooks/useLogin";
|
||||||
|
import type { LoginPayload, User } from "../components/shared/types/AuthTypes";
|
||||||
|
import { createContext, useContext, type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
login: (data: LoginPayload) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) throw new Error("useAuth must be used within AuthProvider");
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: user, isLoading } = useCurrentUser();
|
||||||
|
const loginMutation = useLogin();
|
||||||
|
|
||||||
|
const login = async (payload: LoginPayload) => {
|
||||||
|
await loginMutation.mutateAsync(payload, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
localStorage.setItem("access_token", data.access_token);
|
||||||
|
queryClient.setQueryData(["me"], data.user);
|
||||||
|
toast.success(`Welcome ${data.user.username}`);
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
toast.error(err.response?.data?.message || "Login failed");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
queryClient.removeQueries({ queryKey: ["me"] });
|
||||||
|
toast.info("Logged out");
|
||||||
|
window.location.href = "/login";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{ user: user ?? null, login, logout, isLoading }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/hooks/useCurrentUser.ts
Normal file
12
src/hooks/useCurrentUser.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import type { User } from "../components/shared/types/AuthTypes";
|
||||||
|
import { fetchMe } from "../api/authApi";
|
||||||
|
|
||||||
|
export const useCurrentUser = () =>
|
||||||
|
useQuery<User>({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
enabled: !!localStorage.getItem("access_token"),
|
||||||
|
retry: false,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
11
src/hooks/useLogin.ts
Normal file
11
src/hooks/useLogin.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import type {
|
||||||
|
LoginPayload,
|
||||||
|
LoginResponse,
|
||||||
|
} from "../components/shared/types/AuthTypes";
|
||||||
|
import { loginRequest } from "../api/authApi";
|
||||||
|
|
||||||
|
export const useLogin = () =>
|
||||||
|
useMutation<LoginResponse, any, LoginPayload>({
|
||||||
|
mutationFn: loginRequest,
|
||||||
|
});
|
||||||
64
src/pages/LoginPage/LoginPage.tsx
Normal file
64
src/pages/LoginPage/LoginPage.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
import Container from "../../components/shared/Container";
|
||||||
|
import { Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const { login, user } = useAuth();
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}, [user, navigate]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await login({ email, password });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box sx={{ padding: 4, maxWidth: 360, margin: "auto" }}>
|
||||||
|
<Typography variant="h4" mb={3} textAlign="center">
|
||||||
|
Login
|
||||||
|
</Typography>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: "10px" }}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
43
src/pages/ProfilePage/Profilepage.tsx
Normal file
43
src/pages/ProfilePage/Profilepage.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
import Container from "../../components/shared/Container";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const ProfilePage = () => {
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box sx={{ padding: 4, maxWidth: 360 }}>
|
||||||
|
<Typography variant="h4" mb={2}>
|
||||||
|
Profile
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" mb={2}>
|
||||||
|
Username: {user?.username}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" mb={2}>
|
||||||
|
E-mail: {user?.email}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" mb={2}>
|
||||||
|
Role: {user?.type}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" color="primary" onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
{user?.type === "admin" && (
|
||||||
|
<Button
|
||||||
|
sx={{marginLeft: "10px"}}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => navigate("/admin")}
|
||||||
|
>
|
||||||
|
Admin Dashboard
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
18
src/pages/Unauthorized/Unauthorized.tsx
Normal file
18
src/pages/Unauthorized/Unauthorized.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export default function Unauthorized() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Typography variant="h2" color="error" mb={2}>
|
||||||
|
401
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5">Unauthorized</Typography>
|
||||||
|
<Typography>You do not have access to this page.</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,23 +1,33 @@
|
|||||||
import { createBrowserRouter } from "react-router-dom";
|
import { createBrowserRouter } from "react-router-dom";
|
||||||
import MainLayout from "../layouts/MainLayout/MainLayout";
|
import MainLayout from "../layouts/MainLayout/MainLayout";
|
||||||
import IndexPage from "../pages/IndexPage/IndexPage";
|
import IndexPage from "../pages/IndexPage/IndexPage";
|
||||||
import QuestionsPage from "../pages/QuestionsPage/QuestionsPage";
|
import LoginPage from "../pages/LoginPage/LoginPage";
|
||||||
|
import ProfilePage from "../pages/ProfilePage/Profilepage";
|
||||||
|
import { ProtectedRoute } from "../components/ProtectedRoute/ProtectedRoute";
|
||||||
|
|
||||||
const router = createBrowserRouter ([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path:'/',
|
path: "/",
|
||||||
element: <MainLayout/>,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index:true,
|
index: true,
|
||||||
element: <IndexPage/>
|
element: <IndexPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/questions",
|
path: "/login",
|
||||||
element: <QuestionsPage/>
|
element: <LoginPage />,
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
}
|
path: "/profile",
|
||||||
])
|
element: (
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ProfilePage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user