코딩/React

NextJS와 MongoDB 4편 Chakra-UI 적용하기

드리프트 2021. 9. 15. 14:17
728x170

 

 

안녕하세요?

 

지난 시간에 이어 NextJS와 MongoDB를 이용한 유저 로그인 세션 구현하기 4편을 계속 이어 나가도록 하겠습니다.

 

1편 : https://cpro95.tistory.com/463

 

NextJS와 MongoDB로 유저 로그인 세션 구현하기 1편

안녕하세요? 오늘부터 새로운 NextJS 강좌를 시작해 볼까 합니다. 일반 ReactJS 앱이 아닌 최근 들어 ReactJS 프로트엔드에서 가장 각광받고 있는 NextJS를 이용할 예정인데요. NextJS는 Serverless 앱 구현에

cpro95.tistory.com

 

2편 : https://cpro95.tistory.com/465

 

NextJS와 MongoDB로 유저 로그인 세션 구현하기 2편

안녕하세요? 지난 시간에 이어 NextJS와 MongoDB를 이용한 유저 로그인 세션 구현하기 2편을 계속 이어 나가도록 하겠습니다. 1편: https://cpro95.tistory.com/463 NextJS와 MongoDB로 유저 로그인 세션 구현하기.

cpro95.tistory.com

 

3편 : https://cpro95.tistory.com/478

 

NextJS와 MongoDB로 유저 로그인 세션 구현하기 3편

안녕하세요? 지난 시간에 이어 NextJS와 MongoDB를 이용한 유저 로그인 세션 구현하기 3편을 계속 이어 나가도록 하겠습니다. 1편 : https://cpro95.tistory.com/463 NextJS와 MongoDB로 유저 로그인 세션 구현하..

cpro95.tistory.com

 

1편에서는 기본 준비작업을 했었고,

 

2편에서는 로그인, 로그아웃

 

3편에서는 유저 정보 업데이트와 패스워드 변경까지 알아보았습니다.

 

4편에서는 UI 부분을 Chakra-ui로 적용할 예정입니다.

 

 

Chakra-UI 인스톨 하기

 

요즘 뜨고 있는 React UI 라이브러리 중에 Chakra-UI가 있습니다.

 

한번 써보고 느낀 점은 tailwindcss 같은 미세한 조정을 쉽게 할 수 있고, 기본적은 Component가 구비되어 있어 더 이상 Material-UI 같은 무거운 UI가 필요 없을 거 같습니다.

 

https://chakra-ui.com/

 

Chakra UI - A simple, modular and accessible component library that gives you the building blocks you need to build your React a

Simple, Modular and Accessible UI Components for your React Applications. Built with Styled System

chakra-ui.com

 

일단 기존 앱에 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에 대해 살펴보았습니다.

 

 

 

 

 

 

그리드형