fix(dashboard): navigation issues
This commit is contained in:
parent
f1b4141367
commit
a596d91ce0
|
@ -1,44 +1,44 @@
|
||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
|
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { createClient, Provider } from "urql";
|
import { createClient, Provider } from 'urql';
|
||||||
import {AppRoutes} from './routes'
|
import { AppRoutes } from './routes';
|
||||||
import { AuthContainer } from "./containers/AuthContainer";
|
import { AuthContextProvider } from './contexts/AuthContext';
|
||||||
|
|
||||||
const queryClient = createClient({
|
const queryClient = createClient({
|
||||||
url: "/graphql",
|
url: '/graphql',
|
||||||
fetchOptions: () => {
|
fetchOptions: () => {
|
||||||
return {
|
return {
|
||||||
credentials: "include",
|
credentials: 'include',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = extendTheme({
|
const theme = extendTheme({
|
||||||
styles: {
|
styles: {
|
||||||
global: {
|
global: {
|
||||||
"html, body, #root": {
|
'html, body, #root': {
|
||||||
height: "100%",
|
height: '100%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
blue: {
|
blue: {
|
||||||
500: "rgb(59,130,246)",
|
500: 'rgb(59,130,246)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<Provider value={queryClient}>
|
<Provider value={queryClient}>
|
||||||
<BrowserRouter basename="/dashboard">
|
<BrowserRouter basename="/dashboard">
|
||||||
<AuthContainer>
|
<AuthContextProvider>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</AuthContainer>
|
</AuthContextProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,78 @@
|
||||||
import { Box, Image, Link, Text } from "@chakra-ui/react";
|
import { Box, Image, Link, Text, Button } from '@chakra-ui/react';
|
||||||
import { NavLink, useLocation } from "react-router-dom";
|
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { LOGO_URL } from "../constants";
|
import { LOGO_URL } from '../constants';
|
||||||
|
import { useMutation } from 'urql';
|
||||||
|
import { AdminLogout } from '../graphql/mutation';
|
||||||
|
import { useAuthContext } from '../contexts/AuthContext';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
route: "/users",
|
route: '/users',
|
||||||
name: "Users",
|
name: 'Users',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: "/settings",
|
route: '/',
|
||||||
name: "Settings",
|
name: 'Environment Variable',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
return (
|
const [_, logout] = useMutation(AdminLogout);
|
||||||
<Box as="nav" h="100%">
|
const { setIsLoggedIn } = useAuthContext();
|
||||||
<NavLink to="/">
|
const navigate = useNavigate();
|
||||||
<Box d="flex" alignItems="center" p="4" mt="4" mb="10">
|
|
||||||
<Image w="8" src={LOGO_URL} alt="" />
|
const handleLogout = async () => {
|
||||||
<Text
|
await logout();
|
||||||
color="white"
|
setIsLoggedIn(false);
|
||||||
casing="uppercase"
|
navigate('/', { replace: true });
|
||||||
fontSize="1xl"
|
};
|
||||||
ml="3"
|
|
||||||
letterSpacing="1.5px"
|
return (
|
||||||
>
|
<Box as="nav" h="100%">
|
||||||
Authorizer
|
<NavLink to="/">
|
||||||
</Text>
|
<Box d="flex" alignItems="center" p="4" mt="4" mb="10">
|
||||||
</Box>
|
<Image w="8" src={LOGO_URL} alt="" />
|
||||||
</NavLink>
|
<Text
|
||||||
{routes.map(({ route, name }: any) => (
|
color="white"
|
||||||
<Link
|
casing="uppercase"
|
||||||
color={pathname === route ? "blue.500" : "white"}
|
fontSize="1xl"
|
||||||
transition="all ease-in 0.2s"
|
ml="3"
|
||||||
_hover={{ color: pathname === route ? "blue.200" : "whiteAlpha.700" }}
|
letterSpacing="1.5px"
|
||||||
px="4"
|
>
|
||||||
py="2"
|
Authorizer
|
||||||
bg={pathname === route ? "white" : ""}
|
</Text>
|
||||||
fontWeight="bold"
|
</Box>
|
||||||
display="block"
|
</NavLink>
|
||||||
as={NavLink}
|
{routes.map(({ route, name }: any) => (
|
||||||
key={name}
|
<Link
|
||||||
to={route}
|
color={pathname === route ? 'blue.500' : 'white'}
|
||||||
>
|
transition="all ease-in 0.2s"
|
||||||
{name}
|
_hover={{ color: pathname === route ? 'blue.200' : 'whiteAlpha.700' }}
|
||||||
</Link>
|
px="4"
|
||||||
))}
|
py="2"
|
||||||
</Box>
|
bg={pathname === route ? 'white' : ''}
|
||||||
);
|
fontWeight="bold"
|
||||||
|
display="block"
|
||||||
|
as={NavLink}
|
||||||
|
key={name}
|
||||||
|
to={route}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
as="div"
|
||||||
|
w="100%"
|
||||||
|
position="absolute"
|
||||||
|
bottom="5"
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Button onClick={handleLogout}>Logout</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function AuthLayout() {
|
|
||||||
return <h1>Auth Layout</h1>;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function DefaultLayout() {
|
|
||||||
return <h1>Default Layout</h1>;
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { Center, Spinner } from "@chakra-ui/react";
|
|
||||||
import React from "react";
|
|
||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
import { AdminSessionQuery } from "../graphql/queries";
|
|
||||||
import { hasAdminSecret } from "../utils";
|
|
||||||
|
|
||||||
export const AuthContainer = ({ children }: { children: any }) => {
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
const isOnboardingComplete = hasAdminSecret();
|
|
||||||
const [result] = useQuery({
|
|
||||||
query: AdminSessionQuery,
|
|
||||||
pause: !isOnboardingComplete,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.fetching) {
|
|
||||||
return (
|
|
||||||
<Center>
|
|
||||||
<Spinner />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
result?.error?.message.includes("unauthorized") &&
|
|
||||||
pathname !== "/login"
|
|
||||||
) {
|
|
||||||
return <Navigate to="login" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOnboardingComplete && pathname !== "/setup") {
|
|
||||||
return <Navigate to="setup" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
|
import { Center, Spinner } from '@chakra-ui/react';
|
||||||
|
import { useQuery } from 'urql';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AdminSessionQuery } from '../graphql/queries';
|
||||||
|
import { hasAdminSecret } from '../utils';
|
||||||
|
|
||||||
|
const AuthContext = createContext({
|
||||||
|
isLoggedIn: false,
|
||||||
|
setIsLoggedIn: (data: boolean) => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AuthContextProvider = ({ children }: { children: any }) => {
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isOnboardingComplete = hasAdminSecret();
|
||||||
|
const [{ fetching, data, error }] = useQuery({
|
||||||
|
query: AdminSessionQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetching && !error) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
if (pathname === '/login' || pathname === 'signup') {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fetching, error]);
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthContext = () => useContext(AuthContext);
|
|
@ -12,4 +12,12 @@ mutation adminLogin($secret: String!){
|
||||||
message
|
message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
|
export const AdminLogout = `
|
||||||
|
mutation adminLogout {
|
||||||
|
_admin_logout {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
import { Box, Center, Flex, Image, Text } from "@chakra-ui/react";
|
import { Box, Center, Flex, Image, Text } from '@chakra-ui/react';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { LOGO_URL } from "../constants";
|
import { LOGO_URL } from '../constants';
|
||||||
|
|
||||||
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Flex flexWrap="wrap" h="100%">
|
<Flex flexWrap="wrap" h="100%">
|
||||||
<Center h="100%" flex="3" bg="blue.500" flexDirection="column">
|
<Center h="100%" flex="2" bg="blue.500" flexDirection="column">
|
||||||
<Image
|
<Image src={LOGO_URL} alt="" />
|
||||||
src={LOGO_URL}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
color="white"
|
color="white"
|
||||||
casing="uppercase"
|
casing="uppercase"
|
||||||
fontSize="3xl"
|
fontSize="3xl"
|
||||||
mt="2"
|
mt="2"
|
||||||
letterSpacing="2.25px"
|
letterSpacing="2.25px"
|
||||||
>
|
>
|
||||||
Authorizer
|
Authorizer
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
<Center h="100%" flex="2">
|
<Center h="100%" flex="2">
|
||||||
{children}
|
{children}
|
||||||
</Center>
|
</Center>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { Box, Flex } from "@chakra-ui/react";
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Sidebar } from "../components/Sidebar";
|
import { Sidebar } from '../components/Sidebar';
|
||||||
|
|
||||||
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Flex flexWrap="wrap" h="100%">
|
<Flex flexWrap="wrap" h="100%">
|
||||||
<Box maxW="72" bg="blue.500" flex="1">
|
<Box w="72" bg="blue.500" flex="1" position="fixed" h="100vh">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</Box>
|
</Box>
|
||||||
<Box as="main" flex="2" p="10">{children}</Box>
|
<Box as="main" flex="2" p="10" marginLeft="72">
|
||||||
</Flex>
|
{children}
|
||||||
);
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +1,95 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
useToast,
|
useToast,
|
||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from '@chakra-ui/react';
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react';
|
||||||
import { useMutation } from "urql";
|
import { useMutation } from 'urql';
|
||||||
import { AuthLayout } from "../layouts/AuthLayout";
|
|
||||||
import { AdminLogin, AdminSignup } from "../graphql/mutation";
|
import { AuthLayout } from '../layouts/AuthLayout';
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { AdminLogin, AdminSignup } from '../graphql/mutation';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuthContext } from '../contexts/AuthContext';
|
||||||
|
import { capitalizeFirstLetter, hasAdminSecret } from '../utils';
|
||||||
|
|
||||||
export const Auth = () => {
|
export const Auth = () => {
|
||||||
const [loginResult, login] = useMutation(AdminLogin);
|
const [loginResult, login] = useMutation(AdminLogin);
|
||||||
const [signUpResult, signup] = useMutation(AdminSignup);
|
const [signUpResult, signup] = useMutation(AdminSignup);
|
||||||
|
const { setIsLoggedIn } = useAuthContext();
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const { pathname } = useLocation();
|
const isLogin = hasAdminSecret();
|
||||||
const isLogin = pathname === "/login";
|
|
||||||
|
|
||||||
const handleAdminSecret = (e: any) => {
|
const handleSubmit = (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formValues = [...e.target.elements].reduce((agg: any, elem: any) => {
|
const formValues = [...e.target.elements].reduce((agg: any, elem: any) => {
|
||||||
if (elem.id) {
|
if (elem.id) {
|
||||||
return {
|
return {
|
||||||
...agg,
|
...agg,
|
||||||
[elem.id]: elem.value,
|
[elem.id]: elem.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return agg;
|
return agg;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
(isLogin ? login : signup)({
|
(isLogin ? login : signup)({
|
||||||
secret: formValues["admin-secret"],
|
secret: formValues['admin-secret'],
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (!res.error?.name) {
|
setIsLoggedIn(true);
|
||||||
navigate("/");
|
if (res.data) {
|
||||||
}
|
navigate('/', { replace: true });
|
||||||
});
|
}
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const errors = isLogin ? loginResult.error : signUpResult.error;
|
const errors = isLogin ? loginResult.error : signUpResult.error;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (errors?.graphQLErrors) {
|
if (errors?.graphQLErrors) {
|
||||||
(errors?.graphQLErrors || []).map((error: any) => {
|
(errors?.graphQLErrors || []).map((error: any) => {
|
||||||
toast({
|
toast({
|
||||||
title: error.message,
|
title: capitalizeFirstLetter(error.message),
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
status: "error",
|
status: 'error',
|
||||||
position:"bottom-right"
|
position: 'bottom-right',
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [errors])
|
}, [errors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout>
|
<AuthLayout>
|
||||||
<form onSubmit={handleAdminSecret}>
|
<form onSubmit={handleSubmit}>
|
||||||
<VStack spacing="2.5" justify="space-between">
|
<VStack spacing="2.5" justify="space-between">
|
||||||
<FormControl isRequired>
|
<FormControl isRequired>
|
||||||
<FormLabel htmlFor="admin-secret">
|
<FormLabel htmlFor="admin-secret">
|
||||||
{isLogin ? "Enter" : "Setup"} Admin Secret
|
{isLogin ? 'Enter' : 'Setup'} Admin Secret
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
size="lg"
|
size="lg"
|
||||||
id="admin-secret"
|
id="admin-secret"
|
||||||
placeholder="Admin secret"
|
placeholder="Admin secret"
|
||||||
minLength={6}
|
type="password"
|
||||||
/>
|
minLength={!isLogin ? 6 : 1}
|
||||||
</FormControl>
|
/>
|
||||||
<Button
|
</FormControl>
|
||||||
isLoading={signUpResult.fetching || loginResult.fetching}
|
<Button
|
||||||
colorScheme="blue"
|
isLoading={signUpResult.fetching || loginResult.fetching}
|
||||||
size="lg"
|
colorScheme="blue"
|
||||||
w="100%"
|
size="lg"
|
||||||
d="block"
|
w="100%"
|
||||||
type="submit"
|
d="block"
|
||||||
>
|
type="submit"
|
||||||
{isLogin ? "Login" : "Sign up"}
|
>
|
||||||
</Button>
|
{isLogin ? 'Login' : 'Sign up'}
|
||||||
</VStack>
|
</Button>
|
||||||
</form>
|
</VStack>
|
||||||
</AuthLayout>
|
</form>
|
||||||
);
|
</AuthLayout>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,26 +1,34 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Outlet, Route, Routes } from "react-router-dom";
|
import { Outlet, Route, Routes } from 'react-router-dom';
|
||||||
import { DashboardLayout } from "../layouts/DashboardLayout";
|
|
||||||
import { Auth } from "../pages/Auth";
|
|
||||||
|
|
||||||
import { Home } from "../pages/Home";
|
import { useAuthContext } from '../contexts/AuthContext';
|
||||||
import { Users } from "../pages/Users";
|
import { DashboardLayout } from '../layouts/DashboardLayout';
|
||||||
|
import { Auth } from '../pages/Auth';
|
||||||
|
import { Home } from '../pages/Home';
|
||||||
|
import { Users } from '../pages/Users';
|
||||||
|
|
||||||
export const AppRoutes = () => {
|
export const AppRoutes = () => {
|
||||||
return (
|
const { isLoggedIn } = useAuthContext();
|
||||||
<Routes>
|
|
||||||
<Route path="login" element={<Auth />} />
|
if (isLoggedIn) {
|
||||||
<Route path="setup" element={<Auth />} />
|
return (
|
||||||
<Route
|
<Routes>
|
||||||
element={
|
<Route
|
||||||
<DashboardLayout>
|
element={
|
||||||
<Outlet />
|
<DashboardLayout>
|
||||||
</DashboardLayout>
|
<Outlet />
|
||||||
}
|
</DashboardLayout>
|
||||||
>
|
}
|
||||||
<Route path="/" element={<Home />} />
|
>
|
||||||
<Route path="users" element={<Users />} />
|
<Route path="/" element={<Home />} />
|
||||||
</Route>
|
<Route path="users" element={<Users />} />
|
||||||
</Routes>
|
</Route>
|
||||||
);
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Auth />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
export const hasAdminSecret = () => {
|
export const hasAdminSecret = () => {
|
||||||
return (<any>window)["__authorizer__"].isOnboardingCompleted === true
|
return (<any>window)['__authorizer__'].isOnboardingCompleted === true;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const capitalizeFirstLetter = (data: string): string =>
|
||||||
|
data.charAt(0).toUpperCase() + data.slice(1);
|
||||||
|
|
|
@ -39,6 +39,7 @@ func InitRouter() *gin.Engine {
|
||||||
{
|
{
|
||||||
app.Static("/build", "dashboard/build")
|
app.Static("/build", "dashboard/build")
|
||||||
app.GET("/", handlers.DashboardHandler())
|
app.GET("/", handlers.DashboardHandler())
|
||||||
|
app.GET("/:page", handlers.DashboardHandler())
|
||||||
}
|
}
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user