fix(dashboard): navigation issues

This commit is contained in:
Lakhan Samani 2022-01-17 13:03:28 +05:30
parent f1b4141367
commit a596d91ce0
13 changed files with 320 additions and 270 deletions

View File

@ -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>
); );
} }

View File

@ -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>
);
}; };

View File

@ -1,5 +0,0 @@
import React from 'react';
export default function AuthLayout() {
return <h1>Auth Layout</h1>;
}

View File

@ -1,5 +0,0 @@
import React from 'react';
export default function DefaultLayout() {
return <h1>Default Layout</h1>;
}

View File

@ -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;
};

View File

@ -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);

View File

@ -12,4 +12,12 @@ mutation adminLogin($secret: String!){
message message
} }
} }
` `;
export const AdminLogout = `
mutation adminLogout {
_admin_logout {
message
}
}
`;

View File

@ -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>
); );
} }

View File

@ -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>
);
} }

View File

@ -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>
);
}; };

View File

@ -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>
);
}; };

View File

@ -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);

View File

@ -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
} }