안녕하세요?
지난 시간에 이어 NextJS와 MongoDB를 이용한 유저 로그인 세션 구현하기 4편을 계속 이어 나가도록 하겠습니다.
1편 : https://cpro95.tistory.com/463
2편 : https://cpro95.tistory.com/465
3편 : https://cpro95.tistory.com/478
1편에서는 기본 준비작업을 했었고,
2편에서는 로그인, 로그아웃
3편에서는 유저 정보 업데이트와 패스워드 변경까지 알아보았습니다.
4편에서는 UI 부분을 Chakra-ui로 적용할 예정입니다.
Chakra-UI 인스톨 하기
요즘 뜨고 있는 React UI 라이브러리 중에 Chakra-UI가 있습니다.
한번 써보고 느낀 점은 tailwindcss 같은 미세한 조정을 쉽게 할 수 있고, 기본적은 Component가 구비되어 있어 더 이상 Material-UI 같은 무거운 UI가 필요 없을 거 같습니다.
일단 기존 앱에 chakra-ui를 npm install 해 보겠습니다.
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
뭔가 많이 설치한다고 생각할 수 있는데요.
Chakra-ui 자체가 emotion 같이 styled component를 이용해서 만들었기 때문입니다.
그리고 우리는 icon도 필요하기 때문에 다음과 같이 icon 설치도 병행하겠습니다.
npm install @chakra-ui/icons
이제 @chakra-ui를 위해 기본적인 App의 레이아웃을 변경토록 하겠습니다.
/pages/_app.js
import React from "react";
import Layout from "../components/layout";
import { ChakraProvider } from "@chakra-ui/react";
export default function MyApp({ Component, pageProps }) {
return (
<ChakraProvider resetCSS>
<Layout>
<Component {...pageProps} />
</Layout>
</ChakraProvider>
);
}
Chakra-ui는 기본적으로 ChakraProvider를 wrapping 해줘야 합니다.
위 코드에서 우리는 Layout 컴포넌트를 직접 넣어 줬는데요.
Layout 컴포넌트의 코드는 아래와 같습니다.
/components/Layout.js
import React from "react";
import Head from "next/head";
import Header from "./Header";
import { Container } from "@chakra-ui/react";
export default function Layout(props) {
return (
<>
<Head>
<title>NextJS Mongodb user login session example</title>
<meta
key="viewport"
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta
name="description"
content="Hello"
/>
<meta property="og:title" content="NextJS Mongodb" />
<meta
property="og:description"
content="Hi"
/>
<meta
property="og:image"
content="/default-profile.jpg"
/>
</Head>
<Container
maxW="container.xl"
p={0}
>
<Header />
{props.children}
</Container>
</>
);
}
Layout 컴포넌트는 간단합니다.
앞으로 우리의 모든 컴포넌트들은 Container 밑에 둔다는 얘기입니다.
Chakra-UI에서 Container는 기본적인 컴포넌트로 maxWidth는 container.xl로 지정했습니다.
container.xl 은 Chakra-UI가 기본으로 세팅된 컨테이너의 크기를 정한 변수인데요.
default Theme에 있습니다.
아래 스크린숏에서 보시면 default로 sizes 객체와 container 객체가 있습니다.
container.xl이라고 maxWidth를 지정하면 앞으로 우리 페이지는 1280px 보다 더 크게는 보이지 말라는 뜻이 되는 거죠.
그리고 Header 컴포넌트도 만들었는데요.
직접 보시죠.
/components/Header.js
import { Flex, Text, Box, Stack } from "@chakra-ui/react";
import { useState } from "react";
import { useCurrentUser } from "../hooks/index";
import { DarkModeSwitch } from "./DarkModeSwitch";
import Link from "next/link";
import { CloseIcon, HamburgerIcon } from "@chakra-ui/icons";
const MenuItem = ({ children, onClick, to = "/" }) => {
return (
<Text
ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
display="block"
onClick={onClick}
>
<Link href={to}>{children}</Link>
</Text>
);
};
const Header = () => {
const [user, { mutate }] = useCurrentUser();
const [show, setShow] = useState(false);
const toggleMenu = () => setShow(!show);
const handleLogout = async () => {
await fetch("/api/auth", {
method: "DELETE",
});
mutate(null);
toggleMenu();
};
return (
<Flex
// mb={4}
p={2}
as="nav"
align="center"
alignItems="center"
justify="space-between"
wrap="wrap"
w="100%"
>
<Box w="200px">
<Text fontSize="lg" fontWeight="bold">
<Link href="/">NextJS MongoDB Example</Link>
</Text>
</Box>
<Box display={{ base: "block", md: "none" }} onClick={toggleMenu}>
{show ? <CloseIcon /> : <HamburgerIcon />}
</Box>
<Box
display={{ base: show ? "block" : "none", md: "block" }}
flexBasis={{ base: "100%", md: "auto" }}
>
<Stack
spacing={8}
align="center"
justify={["center", "space-between", "flex-end", "flex-end"]}
direction={["column", "row", "row", "row"]}
pt={[4, 4, 0, 0]}
>
{!user ? (
<>
<MenuItem onClick={toggleMenu} to="/login">
Log in
</MenuItem>
<MenuItem onClick={toggleMenu} to="/signup">
Sign up
</MenuItem>
</>
) : (
<>
{user.admin ? (
<>
<MenuItem onClick={toggleMenu} to={"/admin"}>
Admin
</MenuItem>
</>
) : (
<></>
)}
<MenuItem onClick={toggleMenu} to={`/user/${user._id}`}>
Profile
</MenuItem>
<Text
ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
display="block"
onClick={handleLogout}
>
<Link href="/">Logout</Link>
</Text>
</>
)}
<DarkModeSwitch />
</Stack>
</Box>
</Flex>
);
};
export default Header;
Header 부분은 반응형 웹페이지 구현을 위해 햄버거 메뉴가 작동토록 만들었습니다.
당연히 Flex 항목을 이용했고요.
Flex 컴포넌트는 Chakra-UI가 제공하는 가장 강력한 컴포넌트로 CSS3의 display: flex를 구현한 겁니다.
그리고 Chakra-UI에서 가장 마음에 드는 DarkMode 기능인데요.
DarkModeSwitch 컴포넌트를 살펴본 후 자세히 알아보겠습니다.
/components/DarkModeSwitch.js
import { useColorMode, IconButton } from "@chakra-ui/react";
import { MoonIcon, SunIcon } from "@chakra-ui/icons";
export const DarkModeSwitch = () => {
const { colorMode, toggleColorMode } = useColorMode();
return (
<IconButton
aria-label="Toggle Dark Switch"
icon={colorMode === "dark" ? <SunIcon /> : <MoonIcon />}
onClick={toggleColorMode}
ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
display="block"
w={10}
h={10}
/>
);
};
Chakra-UI는 다크 모드를 간단하게 지원하는데요.
useColorMode라는 커스텀 훅(hook)을 제공해 줍니다.
간단하게 아이콘을 클릭만 하면 다크 모드 라이트 모드를 왔다 갔다 할 수 있는 강력한 기능입니다.
지금까지 만든 걸 한 번 볼까요?
데스크톱 모드일 경우입니다.
반응형 모드일 때 즉, 모바일에서 볼 때입니다.
햄버거 메뉴를 눌렀을 때입니다.
다크 모드 스위치를 눌렀을 때 모드입니다.
어떤가요? 깔끔하지 않나요?
그럼 본격적으로 지난번에 작성한 BootstrapUI 부분을 Chakra-UI로 바꿔 나가겠습니다.
/pages/login.js
import React, { useState, useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useCurrentUser } from "@/hooks/index";
import {
Flex,
VStack,
Button,
Input,
Heading,
Text,
} from "@chakra-ui/react";
const LoginPage = () => {
const router = useRouter();
const [errorMsg, setErrorMsg] = useState("");
const [user, { mutate }] = useCurrentUser();
useEffect(() => {
// redirect to home if user is authenticated
if (user) router.push("/");
}, [user]);
async function onSubmit(e) {
e.preventDefault();
const body = {
email: e.currentTarget.email.value,
password: e.currentTarget.password.value,
};
const res = await fetch("/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.status === 200) {
const userObj = await res.json();
mutate(userObj);
} else {
setErrorMsg("Incorrect username or password. Try again!");
}
}
return (
<>
<Head>
<title>Sign in</title>
</Head>
<Flex align="center" justify="center">
<Flex w={["80%", "80%", "60%", "50%", "50%"]} p={[0, 10, 20]}>
<VStack>
{errorMsg ? (
<Text align="center" color="red">
{errorMsg}
</Text>
) : null}
<Heading>Sign In</Heading>
<form onSubmit={onSubmit}>
<Input
m={1}
id="email"
type="email"
name="email"
placeholder="Email address"
size="lg"
></Input>
<Input
m={1}
id="password"
type="password"
name="password"
placeholder="Password"
size="lg"
></Input>
<Button m={1} w="100%" type="submit" colorScheme="twitter"
size="lg">
Log In
</Button>
</form>
</VStack>
</Flex>
</Flex>
</>
);
};
export default LoginPage;
로그인 화면입니다.
영어로 되어 있는 부분은 직접 한글로 고치시면 됩니다.
저는 개인적으로 혼자 보려고 하는 사이트는 영어로 작성하는 편입니다. 영어 공부 좀 더 할 수 있지 않을까요?
login 코드에서 눈여겨봐야 할 부분은 바로 Flex 컴포넌트의 w 항목입니다.
<Flex w={["80%", "80%", "60%", "50%"]}>
w에 배열이 들어가 있습니다.
이 배열에는 Chakra-UI가 제공하는 브레이크 포인트에 해당하는 값을 넣을 수 있는데요.
Chakra-UI가 제공하는 기본 브레이크 포인트는 아래와 같습니다.
const breakpoints = createBreakpoints({
sm: "30em",
md: "48em",
lg: "62em",
xl: "80em",
"2xl": "96em",
})
즉, 사이즈가 sm, md, lg, xl, 2xl 같이 창의 크기에 따라 어떻게 반응할지 그 기준이 되는 크기를 미리 정해놓았는데요.
우리는 창이 sm 사이즈일 때는 width를 80%, md 사이즈일 때는 80%, lg 사이즈 일 때는 60%, 그리고 그 이상 사이즈일 때는 50%라고 지정했습니다.
창을 한번 줄였다가 늘렸다가 해보시면 로그인 화면의 Input 칸이 줄었다 늘었다 하는 걸 볼 수 있을 겁니다.
그리고 Input 컴포넌트에서 볼 수 있듯이 m={1} 이란 항목이 있는데요.
추측하시는 데로 margin 이란 뜻입니다.
당연히 p는 padding 이겠죠.
아래 그림은 Chakra-UI에서 지원하는 CSS 항목입니다. 참고 바랍니다.
/pages/signup.js
import React, { useState, useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useCurrentUser } from "@/hooks/index";
import {
Flex,
Button,
Input,
Text,
VStack,
Heading,
} from "@chakra-ui/react";
const SignupPage = () => {
const router = useRouter();
const [user, { mutate }] = useCurrentUser();
const [errorMsg, setErrorMsg] = useState("");
useEffect(() => {
// redirect to home if user is authenticated
if (user) router.replace("/");
}, [router, user]);
const handleSubmit = async (e) => {
e.preventDefault();
const body = {
email: e.currentTarget.email.value,
name: e.currentTarget.name.value,
password: e.currentTarget.password.value,
};
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.status === 201) {
const userObj = await res.json();
mutate(userObj);
} else {
setErrorMsg(await res.text());
}
};
return (
<>
<Head>
<title>Sign up</title>
</Head>
<Flex align="center" justify="center">
<Flex w={["80%", "80%", "60%", "50%", "50%"]} p={[0, 10, 20]}>
<VStack>
{errorMsg ? (
<Text align="center" color="red">
{errorMsg}
</Text>
) : null}
<Heading>
Sign Up
</Heading>
<form onSubmit={handleSubmit}>
<Input
m={1}
id="name"
type="text"
name="name"
placeholder="Name"
size="lg"
></Input>
<Input
m={1}
id="email"
type="email"
name="email"
placeholder="Email address"
size="lg"
></Input>
<Input
m={1}
id="password"
type="password"
name="password"
placeholder="Password"
size="lg"
></Input>
<Button size="lg" m={1} w="100%" type="submit" colorScheme="twitter">
Sign Up
</Button>
</form>
</VStack>
</Flex>
</Flex>
</>
);
};
export default SignupPage;
Signup 페이지도 Login 페이지와 별반 다를 게 없습니다.
/pages/settings.js
import React, { useState, useEffect, useRef } from "react";
import Head from "next/head";
import { useCurrentUser } from "@/hooks/index";
import {
Flex,
FormControl,
FormLabel,
Button,
Input,
Heading,
Text,
VStack,
SimpleGrid,
GridItem,
} from "@chakra-ui/react";
const ProfileSection = () => {
const [user, { mutate }] = useCurrentUser();
const [isUpdating, setIsUpdating] = useState(false);
const nameRef = useRef();
const profilePictureRef = useRef();
const [msg, setMsg] = useState({ message: "", isError: false });
const [msg2, setMsg2] = useState({ message: "", isError: false });
useEffect(() => {
nameRef.current.value = user?.name;
}, [user]);
const handleSubmit = async (event) => {
event.preventDefault();
if (isUpdating) return;
setIsUpdating(true);
const formData = new FormData();
if (profilePictureRef.current.files[0]) {
formData.append("profilePicture", profilePictureRef.current.files[0]);
}
formData.append("name", nameRef.current.value);
const res = await fetch("/api/user", {
method: "PATCH",
body: formData,
});
if (res.status === 200) {
const userData = await res.json();
mutate({
user: {
...user,
...userData.user,
},
});
setMsg({ message: "Profile updated" });
} else {
setMsg({ message: await res.text(), isError: true });
}
setIsUpdating(false);
};
const handleSubmitPasswordChange = async (e) => {
e.preventDefault();
if (
e.currentTarget.newPassword.value !== e.currentTarget.newPassword2.value
) {
setMsg2({ message: "New password do not match each other" });
e.currentTarget.oldPassword.value = "";
e.currentTarget.newPassword.value = "";
e.currentTarget.newPassword2.value = "";
return;
} else {
const body = {
oldPassword: e.currentTarget.oldPassword.value,
newPassword: e.currentTarget.newPassword.value,
newPassword2: e.currentTarget.newPassword2.value,
};
e.currentTarget.oldPassword.value = "";
e.currentTarget.newPassword.value = "";
e.currentTarget.newPassword2.value = "";
const res = await fetch("/api/user/password", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.status === 200) {
setMsg2({ message: "Password updated" });
} else {
setMsg2({ message: await res.text(), isError: true });
}
}
};
return (
<>
<Head>
<title>Settings</title>
</Head>
<Flex align="center" justify="center">
<Flex h={{ base: "auto", md: "100vh" }} py={[0, 10, 20]}
direction={{ base: "column", md: "row" }}
>
<VStack
w="full" h="full" p={10} spacing={10} alignItems="flex-start"
>
<Heading> Profile Edit</Heading>
{msg.message ? (
<Text color={msg.isError ? "red" : "#0070f3"} align="center">
{msg.message}
</Text>
) : null}
<form onSubmit={handleSubmit}>
<SimpleGrid
autoColumns rowGap={3}
>
<GridItem >
<FormControl>
<FormLabel htmlFor="name">Name</FormLabel>
<Input
required
id="name"
name="name"
type="text"
placeholder="Your name"
ref={nameRef}
size="lg"
/>
</FormControl>
</GridItem>
<GridItem>
<FormControl>
<FormLabel htmlFor="avatar">Profile picture</FormLabel>
<Input
type="file"
id="avatar"
name="avatar"
accept="image/png, image/jpeg"
ref={profilePictureRef}
size="lg"
/>
</FormControl>
</GridItem>
<GridItem>
<Button
disabled={isUpdating}
type="submit"
colorScheme="twitter"
size="lg"
w="full"
>
Save
</Button>
</GridItem>
</SimpleGrid>
</form>
</VStack>
<VStack
w="full" h="full" p={10} spacing={10} alignItems="flex-start"
>
<Heading> Password Change</Heading>
{msg2.message ? (
<Text color={msg2.isError ? "red" : "#0070f3"} align="center">
{msg2.message}
</Text>
) : null}
<form onSubmit={handleSubmitPasswordChange}>
<SimpleGrid
autoColumns
rowGap={3}
>
<GridItem>
<FormControl>
<FormLabel htmlFor="oldpassword">Old Password</FormLabel>
<Input
type="password"
name="oldPassword"
id="oldpassword"
required
size="lg"
/>
</FormControl>
</GridItem>
<GridItem>
<FormControl>
<FormLabel htmlFor="newpassword">New Password</FormLabel>
<Input
type="password"
name="newPassword"
id="newpassword"
required
size="lg"
/>
</FormControl>
</GridItem>
<GridItem>
<FormControl>
<FormLabel htmlFor="newpassword2">New Password Repeat</FormLabel>
<Input
type="password"
name="newPassword2"
id="newpassword2"
required
size="lg"
/>
</FormControl>
</GridItem>
<GridItem>
<Button size="lg" w="full" type="submit" colorScheme="twitter">
Change Password
</Button>
</GridItem>
</SimpleGrid>
</form>
</VStack>
</Flex>
</Flex>
</>
);
};
const SettingPage = () => {
const [user] = useCurrentUser();
if (!user) {
return (
<>
<Text fontSize="xl">Found No Authentication! Please Log in</Text>
</>
);
}
return (
<>
<ProfileSection />
</>
);
};
export default SettingPage;
Settings 페이지도 멋지게 꾸몄습니다.
반응형에 맞도록 수정했는데요. 큰 틀에서 보자면
Layout 컴포넌트에 Container 가 있으며,
그 밑에 Flex 컴포넌트가 두 개가 있고, Flex 컴포넌트 밑에는 VStack라는 컴포넌트가 있습니다.
두 개의 Flex 컴포넌트 중 맨 위에 거는 가운데 정렬용입니다.
여기서 VStack 컴포넌트는 Stack 컴포넌트를 수직으로 쌓는 컴포넌트인데요.
Stack 컴포넌트는 동일한 컴포넌트를 묶을 때 아주 유용합니다.
그리고 FormControl 부분은 SimpleGrid를 써서 레이아웃 디자인을 했습니다.
여기서 중요한 반응형 디자인에 대해 짚고 넘어가야 할 부분이 있습니다.
먼저 완성된 페이지를 보겠습니다.
브라우저 창이 데스크톱 모드일 때는 VStack 두 개가 옆으로 나란히 보입니다.
만약 모바일 창으로 변환했을 때는 어떻게 될까요?
VStack 두개가 column으로 보입니다.
밑으로 두개가 쭉 이어져 나오고 있네요.
어떻게 했을까요?
바로 Flex에 있습니다.
<Flex h={{ base: "auto", md: "100vh" }} py={[0, 10, 20]}
direction={{ base: "column", md: "row" }}
>
<VStack />
<VStack />
</Flex>
전체적인 구조는 두 개의 VStack를 가지는 Flex 가 하나 있습니다.
그리고 그 Flex에는 direction 항목이 있는데요.
direction 항목에 객체를 넣어주면 화면 크기 브레이크 포인트에 따라 조절됩니다.
위에 코드를 보시면 base 가 column이고 md 가 row입니다.
즉, 창이 md 사이즈 이상일 경우에는 row로 보여주고 (데스크톱 모드일 경우)
창이 그 이하일 경우 column 형태로 보여준다는 뜻입니다. (모바일 모드일 경우)
이처럼 Chakra-UI는 아주 쉽게 반응형 디자인을 설계할 수 있습니다.
다음으로는 user profile 부분을 살펴보겠습니다.
/pages/user/[userId]/index.js
import React from "react";
import Head from "next/head";
import Error from "next/error";
import { all } from "@/middlewares/index";
import { useCurrentUser } from "@/hooks/index";
import { extractUser } from "@/lib/api-helpers";
import { findUserById } from "@/db/index";
import { defaultProfilePicture } from "@/lib/default";
import {
Box,
Button,
Flex,
Image,
Heading,
VStack,
Table,
Tbody,
Tr, Td,
Tag,
useBreakpointValue,
} from "@chakra-ui/react";
export default function UserPage({ user }) {
if (!user) return <Error statusCode={404} />;
const { name, email, admin, profilePicture, _id } = user || {};
const [currentUser] = useCurrentUser();
const isCurrentUser = currentUser?._id === user._id;
const tableSize = useBreakpointValue({
base: "sm",
sm: "sm",
md: "md",
lg: "lg",
});
return (
<>
<Head>
<title>{name}</title>
</Head>
<Flex h={{ base: "auto", md: "100vh" }} py={[0, 10, 20]}
direction={{ base: "column-reverse", md: "row" }}
>
<VStack
w="full" h="full" p={10} spacing={10} alignItems="flex-start"
>
<Heading> Profile View</Heading>
<Table size={tableSize}>
<Tbody>
<Tr>
<Td><Tag>Name</Tag></Td>
<Td>{name}</Td>
</Tr>
<Tr>
<Td><Tag>Email</Tag></Td>
<Td>{email}</Td>
</Tr>
<Tr>
<Td><Tag>Admin</Tag></Td>
<Td>{admin ? "Yes" : "No"}</Td>
</Tr>
<Tr>
<Td colSpan={2}>
{isCurrentUser && (
// eslint-disable-next-line @next/next/link-passhref
<a href="/settings">
<Button size={tableSize}>
Profile Edit
</Button>
</a>
)}
</Td>
</Tr>
</Tbody>
</Table>
</VStack>
<VStack
w="full" h="full" p={10} spacing={10} alignItems="flex-start"
>
<Heading> Profile Picture</Heading>
<Box w={{ base: "80%", sm: "60%", md: "50%", lg: "80%" }}>
<Image
src={profilePicture || defaultProfilePicture}
alt={name}
size="100%"
rounded="1rem"
shadow="2xl"
/>
</Box>
</VStack>
</Flex>
</>
);
}
export async function getServerSideProps(context) {
await all.run(context.req, context.res);
const user = extractUser(
await findUserById(context.req.db, context.params.userId)
);
if (!user) context.res.statusCode = 404;
return { props: { user } };
}
일단 Settings 페이지랑 전체적인 레이아웃을 똑같습니다.
먼저 전체적인 윤곽입니다.
왼쪽 Profile View 부분입니다.
여기서는 Table을 사용했는데요.
Table size 항목이 평소에 못 보던 방식인데요.
size항목은 보통 "sm", "md", "lg" 세 가지인데요.
tableSize 변수로 지정했습니다.
tableSize는 뭘까요?
const tableSize = useBreakpointValue({
base: "sm",
sm: "sm",
md: "md",
lg: "lg",
});
바로 useBreakpointValue라는 Chakra-UI의 커스텀 훅입니다.
창 크기에 따라 다른 값을 가진다고 할 수 있죠.
그래서 Table 컴포넌트가 창 크기에 따라 크기가 변하는 걸 볼 수 있습니다.
두 번째로는 창을 작게 축소해 볼까요?
두 번째 VStack에 있던 Profile Picture 부분이 상위로 갔습니다.
왜냐하면 Flex direction 부분이 base: "column-reverse" 이기 때문입니다.
정말 간단하지 않나요?
다음으로는 Admin 항목을 살펴보겠습니다.
Admin Dashboard 같은 admin/index.js 파일입니다.
/pages/admin/index.js
import React from "react";
import Link from "next/link";
import Head from "next/head";
import { Flex, Stack, Heading, Button } from "@chakra-ui/react";
import { useCurrentUser } from "@/hooks/index";
const AdminIntroPage = () => {
const [user] = useCurrentUser();
const title = "Admin";
const adminLinks = [
{
link: "/admin/userlist",
label: "User List",
},
];
if (!user || !user.admin) {
return (
<>
<Head>
<title>{title}</title>
</Head>
<p>Please, log in with admin account!</p>
</>
);
} else {
return (
<>
<Head>
<title>{title}</title>
</Head>
<Flex align="center" justify="center">
<Stack direction="column">
<Heading m={8}>Admin Link List</Heading>
<Stack spacing="24px" direction="column">
{adminLinks.map((adminlink) => (
<Link href={adminlink.link}>
<Button size="lg" colorScheme="twitter">
{adminlink.label}
</Button>
</Link>
))}
</Stack>
</Stack>
</Flex>
</>
);
}
};
export default AdminIntroPage;
단순하게 Admin 링크를 위한 버튼을 나타내 줍니다.
그러면 UserList를 본격적으로 알아볼까요?
/pages/admin/userlist.js
import React, { useState, useEffect } from "react";
import Head from "next/head";
import Router from "next/router";
import Error from "next/error";
import { all } from "@/middlewares/index";
import { useCurrentUser } from "@/hooks/index";
import { findUserAll } from "@/db/index";
import {
Checkbox,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Heading,
Flex,
VStack,
useBreakpointValue,
} from "@chakra-ui/react";
export default function UserListPage({ users }) {
const [isUpdating, setIsUpdating] = useState(false);
const [user] = useCurrentUser();
const [msg, setMsg] = useState({ message: "", isError: false });
const tableSize = useBreakpointValue({
base: "sm",
sm: "sm",
md: "md",
lg: "lg",
});
if (!users) return <Error statusCode={404} />;
async function handleSubmit({ _id, admin }) {
if (isUpdating) return;
setIsUpdating(true);
const body = {
_id: _id,
admin: !admin,
};
const res = await fetch("/api/user/admin", {
method: "PATCH",
body: JSON.stringify(body),
});
if (res.status === 200) {
setMsg({ message: "Admin updated" });
} else {
setMsg({ message: await res.text(), isError: true });
}
setIsUpdating(false);
Router.reload(window.location.pathname);
}
if (!user || !user.admin) {
return (
<>
<Head>
<title>UserList</title>
</Head>
<p>Please, Log In</p>
</>
);
} else {
return (
<>
<Head>
<title>UserList</title>
</Head>
<Flex align="center" justify="center">
<Flex h={{ base: "auto", md: "100vh" }} py={[0, 10, 20]}>
<VStack
w="full"
h="full"
p={10}
spacing={10}
alignItems="flex-start"
>
<Heading size="md" m={4}>
Total Users : {users.length}
</Heading>
{msg.message ? (
<p
style={{
color: msg.isError ? "red" : "#0070f3",
textAlign: "center",
}}
>
{msg.message}
</p>
) : null}
<Table size={tableSize} colorScheme="twitter">
<Thead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Admin</Th>
</Tr>
</Thead>
<Tbody>
{users.map((value) => {
return (
<Tr key={value._id}>
<Td>{value.name}</Td>
<Td>{value.email}</Td>
<Td>
<Checkbox
isChecked={value.admin}
onChange={() => handleSubmit(value)}
></Checkbox>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</VStack>
</Flex>
</Flex>
</>
);
}
}
export async function getServerSideProps(context) {
await all.run(context.req, context.res);
const users = await findUserAll(context.req.db);
if (!users) context.res.statusCode = 404;
return { props: { users } };
}
기본적으로 레이아웃이나 디자인은 앞에서 살펴본 거와 똑같습니다.
이상으로 Chakra-UI에 대해 살펴보았습니다.
'코딩 > React' 카테고리의 다른 글
ElectronJS 일렉트론 강좌 1편 SQLite3 sql.js (4) | 2021.09.18 |
---|---|
NextJS MongoDB 5편 SSR, SSG, ISG 예제 (0) | 2021.09.15 |
NextJS Data 가져오는 방법 (SSG, SSR, ISR) (3) | 2021.09.12 |
NextJS와 MongoDB로 유저 로그인 세션 구현하기 3편 (2) | 2021.09.05 |
NextJS와 MongoDB로 유저 로그인 세션 구현하기 2편 (12) | 2021.08.22 |