T</>TahuCoding

Part 3 - Tutorial Next.js 13 Typescript Ecommerce Cart dengan Payment Gateway - Setup Layout Landing & Dashboard

Loading...

Pada tutorial kali ini kita akan belajar menambahkan layout dashboard dan landing page secara general agar kedepan kita lebih mudah fokus ke fitur yang akan dibangun.

Step 1: Membangun Layout Dashboard

Agar dahsboard tidak terdapat container pada root layout app, hapus element main menjadi seperti berikut:

src\app\layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
 
import "./globals.css";
import Provider from "@/provider";
 
const inter = Inter({ subsets: ["latin"] });
 
export const metadata: Metadata = {
  title: "E-Commerce Tahu Coding",
  description: "Modern E-Commerce with latest stack",
};
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}

Pada folder dashboard tambahkan folder dan file berikut:

src
app
dashboard
categories
page.tsx
components
main.tsx
menu.tsx
sidebar.tsx
products
page.tsx
profile
page.tsx
transactions
page.tsx
layout.tsx
page.tsx

Selanjutnya, pada root layout dashboard tambahkan code berikut:

src\app\dashboard\layout.tsx
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
import Main from "./components/main";
 
export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  const session = await getServerSession(OPTIONS);
 
  return <Main session={session}>{children}</Main>;
}

Selanjutnya pada component Main kita akan membentuk layout dashboard yang terdiri dari, header, sidebar, content dan footer, modifikasi component tersebut sebagai berikut:

src\app\dashboard\components\main.tsx
"use client";
 
import React, { ReactNode } from "react";
 
import { Layout, theme } from "antd";
 
import { usePathname } from "next/navigation";
import { Session } from "next-auth";
 
import items from "./menu";
import Sidebar from "./sidebar";
 
const { Header, Content, Footer } = Layout;
 
type Props = {
  children: ReactNode,
  session: Session | null,
};
 
const Main: React.FC<Props> = ({ children, session }) => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();
 
  const pathname = usePathname(); //use this to get url pathname to make sure active page selected when user hard refresh some page in dashboard
 
  return (
    <Layout style={{ minHeight: "100vh" }} hasSider>
      <Sidebar defaultSelectedKeys={[pathname]} items={items} />
      <Layout>
        <Header style={{ padding: 0, background: colorBgContainer }} />
        <Content style={{ margin: "0 16px" }}>
          <div
            style={{
              padding: 24,
              minHeight: 360,
              background: colorBgContainer,
            }}
          >
            {children}
          </div>
        </Content>
        <Footer style={{ textAlign: "center" }}>
          E-Commerce Cart ©2023 Created by Tahu Coding
        </Footer>
      </Layout>
    </Layout>
  );
};
 
export default Main;

Code diatas berfungsi sebagai wrapper content yang ada pada dashboard dan juga rangka dari layout dashboard, kamu bisa lihat author memanggil pathname dan menurunkannya sebagai props pada sidebar agar ketika user melakukan refresh menu yang aktif akan di highlight pada sidebar.

Selanjutnya pada component Sidebar modifikasi menggunakan code berikut:

src\app\dashboard\components\sidebar.tsx
"use client";
 
import React, { useState } from "react";
 
import { Layout, Menu } from "antd";
import { LogoutOutlined } from "@ant-design/icons";
import { signOut } from "next-auth/react";
 
import { MenuItem } from "./menu";
 
const { Sider } = Layout;
 
const Sidebar: React.FC<{
  defaultSelectedKeys: string[],
  items: MenuItem[],
}> = ({ defaultSelectedKeys = [], items = [] }) => {
  const [collapsed, setCollapsed] = useState(false);
 
  //handle logout using next auth
  const handleLogout = () => {
    signOut();
  };
 
  return (
    <Sider
      collapsible
      collapsed={collapsed}
      onCollapse={(value) => setCollapsed(value)}
    >
      <div
        style={{
          padding: "1rem",
          fontWeight: 600,
          textAlign: "center",
          color: "white",
        }}
      >
        <h1>LOGO</h1>
      </div>
      <Menu
        theme="dark"
        defaultSelectedKeys={defaultSelectedKeys}
        defaultOpenKeys={defaultSelectedKeys}
        mode="inline"
        items={items}
      />
      <Menu
        theme="dark"
        defaultSelectedKeys={defaultSelectedKeys}
        defaultOpenKeys={defaultSelectedKeys}
        mode="inline"
        items={[
          {
            key: "logout",
            label: <div onClick={handleLogout}>Logout</div>,
            icon: <LogoutOutlined />,
          },
        ]}
      />
    </Sider>
  );
};
 
export default Sidebar;

Component ini bertanggung jawab sebagai UI yang akan merender menu berdasarkan data yang kita import, selain itu author juga menambahkan fungsi logout dari next auth pada menu paling bawah agar setiap page pada dashboard memiliki akses.

Selanjutnya tambahkan data menu pada file menu.tsx:

src\app\dashboard\components\menu.tsx
import {
  TransactionOutlined,
  AppstoreOutlined,
  DashboardOutlined,
  ProfileOutlined,
  ContainerOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
import Link from "next/link";
 
export type MenuItem = Required<MenuProps>["items"][number];
 
// transform parameter to valid menu object
const getItem = (
  label: React.ReactNode,
  key: string,
  icon?: React.ReactNode,
  children?: MenuItem[]
): MenuItem => {
  const labelLink = <Link href={key}>{label}</Link>;
 
  return {
    key,
    icon,
    children,
    label: labelLink,
  };
};
 
const items: MenuItem[] = [
  getItem("Dashboard", "/dashboard", <DashboardOutlined />),
  getItem("Transactions", "/dashboard/transactions", <TransactionOutlined />),
  getItem("Products", "/dashboard/products", <ContainerOutlined />),
  getItem("Categories", "/dashboard/categories", <AppstoreOutlined />),
  getItem("Profile", "/dashboard/profile", <ProfileOutlined />),
];
 
export default items;

File ini berfungsi sebagai mapper dari menu yang akan kita tampilkan di sisi aplikasi, kamu dapat menggunakan icon apapun sesuai seleramu asal mengimportnya dari Ant Design Icon.

Selanjutnya agar tampilan page dashboard kita simple, hapus component logoutButton.tsx dan modifikasi seperti berikut:

src\app\dashboard\page.tsx
import { getServerSession } from "next-auth";
import React from "react";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
 
const Dashboard = async () => {
  const session = await getServerSession(OPTIONS);
 
  return <div>Welcome: {session?.user?.email}</div>;
};
 
export default Dashboard;

Pada tiap page, yaitu categories, products, profile, transactions cukup tambahkan component placeholder dengan format seperti berikut:

src\app\dashboard\page-name\page.tsx
import React from "react";
 
// please rename based on the name of the page
const PageName = () => {
  return <div>PageName</div>;
};
 
export default PageName;

Jika semua step diatas sudah berhasil, lakukan login maka tampilan dashboard akan menjadi sebagai berikut:

Dashboard

Kamu dapat mencoba beberapa fitur yang ada seperti navigasi antar page, logout hingga collapsed sidebar width dengan mengklik icon panah ke kiri pada bagian bawah sidebar.

Step 2: Adjust Redirect

Karena aplikasi yang kita bikin adalah e-commerce tentunya cukup aneh bukan jika setelah login user diarahkan ke halaman dashboard, maka dari itu kita akan melakukan penyesuaian agar user di redirect ke page landing setelah login

Pada file middleware adjust redirect ketika buka page login atau register ke landing menjadi seperti berikut:

src\middleware.ts
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
 
//this is custom middleware you may improve it as you like
export default async function middleware(req: NextRequest) {
  // Get the pathname of the request (e.g. /, /protected)
  const path = req.nextUrl.pathname;
 
  // If it's the root path, just render it
  if (path === "/") {
    return NextResponse.next();
  }
 
  //decript jwt based on NEXTAUTH_SECRET
  const session = await getToken({
    req,
    secret: process.env.NEXTAUTH_SECRET,
  });
 
  const isProtected = path.includes("/dashboard");
 
  //if jwt token valid then continue, otherwise redirect to login
  if (!session && isProtected) {
    return NextResponse.redirect(new URL("/login", req.url));
  } else if (session && (path === "/login" || path === "/register")) {
    return NextResponse.redirect(new URL("/", req.url));
  }
  return NextResponse.next();
}

Pada component LoginForm modifikasi variable callbackUrl menjadi seperti berikut:

src\app(auth)\login\form.tsx
//...rest of other codes
 
const LoginForm = () => {
  const router = useRouter();
 
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
 
  const searchParams = useSearchParams();
  const callbackUrl = searchParams.get("callbackUrl") || "/";
 
  //...rest of other codes
};
 
export default LoginForm;

Note: jika tampilan login dan register dan landing tidak ada container abaikan dulu.

Step 3: Membangun Layout Landing

Pada root app hapus page.tsx, kemudian tambahkan folder dan file berikut:

src
(auth)
layout.tsx
(landing)
layout.tsx
page.tsx
components
navbar.tsx
nonAuthLayout.tsx

Selanjutnya hapus page.tsx pada root app agar layout landing dapat digrouping dan kita dapat menerapkan container sesuai kebutuhan.

Pada layout landing tambahkan code berikut:

src/app/(landing)/layout.tsx
import NonAuthLayout from "@/components/nonAuthLayout";
 
export default async function Layout({
  children,
}: {
  children: React.ReactNode,
}) {
  return <NonAuthLayout withNavbar>{children}</NonAuthLayout>;
}

Selanjutnya pada page.tsx di folder yg sama tambahkan code berikut:

src/app/(landing)/page.tsx
import React from "react";
 
const Landing = () => {
  return (
    <div className="py-6">
      <section className="bg-slate-600 h-48 rounded-md mb-6 flex items-center justify-center text-white">
        Banner
      </section>
      <section>
        <p className="text-center font-semibold text-2xl mb-6">
          Latest Products
        </p>
        <div className="grid grid-cols-4 gap-4">
          {[...Array(8)]
            .map((_, index) => index + 1)
            .map((item) => (
              <div
                key={item}
                className="h-80 border border-gray-200 rounded-md flex items-center justify-center"
              >
                Product {item}
              </div>
            ))}
        </div>
      </section>
    </div>
  );
};
 
export default Landing;

Selanjutnya pada layout auth modifikasi menjadi seperti berikut:

src/app/(auth)/layout.tsx
import NonAuthLayout from "@/components/nonAuthLayout";
 
export default async function Layout({
  children,
}: {
  children: React.ReactNode,
}) {
  return <NonAuthLayout>{children}</NonAuthLayout>;
}

Tujuan dari menambahkan layout baru pada auth dan landing adalah agar masing-masing dari group tersebut memiliki container.

Pada component NonAuthLayout tambahkan code berikut:

src\components\nonAuthLayout.tsx
import React from "react";
import Navbar from "./navbar";
 
const NonAuthLayout = ({
  children,
  withNavbar = false,
}: {
  children: React.ReactNode,
  withNavbar?: boolean,
}) => {
  return (
    <>
      {withNavbar && <Navbar />}
      <div className="mx-auto max-w-7xl px-8 min-h-screen">{children}</div>
    </>
  );
};
 
export default NonAuthLayout;

Selanjutnya pada Navbar tambahkan logic untuk menampilkan menu login/register atau dashbaord berdasarkan status login.

src\components\navbar.tsx
"use client";
 
import React from "react";
 
import { Button, Skeleton } from "antd";
import Link from "next/link";
import { useSession } from "next-auth/react";
 
const Navbar = () => {
  const { status } = useSession();
 
  return (
    <nav className="h-16 shadow-md flex px-7 items-center justify-between">
      <p className="font-semibold">E-Commerce Tahu Coding</p>
      {status === "unauthenticated" ? (
        <div className="flex gap-2 items-center">
          <Link href="/login">
            <Button type="primary">Login</Button>
          </Link>
          <Link href="/register">
            <Button type="dashed">Register</Button>
          </Link>
        </div>
      ) : status === "authenticated" ? (
        <Link href="/dashboard">
          <Button type="primary">Dashboard</Button>
        </Link>
      ) : (
        <Skeleton.Button active={true} />
      )}
    </nav>
  );
};
 
export default Navbar;

Jika sudah maka tampilan page landing kita akan menjadi seperti berikut.

Dashboard

Bonus: Menentukan Menu Based On role

Jika kamu perhatikan, menu yang tampil pada sidebar tidak ada perbedaan ketika login menggunakan role "admin" atau "user", untuk menghindari hal tersebut kita akan menampilkan menu berdasarkan role pada sisi server side.

Modifikasi file menu.tsx agar support 2 jenis menu:

src\app\dashboard\components\menu.tsx
import {
  TransactionOutlined,
  AppstoreOutlined,
  DashboardOutlined,
  ProfileOutlined,
  ContainerOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
import Link from "next/link";
 
export type MenuItem = Required<MenuProps>["items"][number];
 
const getItem = (
  label: React.ReactNode,
  key: string,
  icon?: React.ReactNode,
  children?: MenuItem[]
): MenuItem => {
  const labelLink = <Link href={key}>{label}</Link>;
 
  return {
    key,
    icon,
    children,
    label: labelLink,
  };
};
 
export const items: MenuItem[] = [
  //for admin
  getItem("Dashboard", "/dashboard", <DashboardOutlined />),
  getItem("Transactions", "/dashboard/transactions", <TransactionOutlined />),
  getItem("Products", "/dashboard/products", <ContainerOutlined />),
  getItem("Categories", "/dashboard/categories", <AppstoreOutlined />),
  getItem("Profile", "/dashboard/profile", <ProfileOutlined />),
];
 
export const userItems: MenuItem[] = [
  //for normal user
  getItem("Dashboard", "/dashboard", <DashboardOutlined />),
  getItem("Transactions", "/dashboard/transactions", <TransactionOutlined />),
  getItem("Profile", "/dashboard/profile", <ProfileOutlined />),
];

Selanjutnya pada layout dashboard tambahkan logic untuk get user data dan get menu berdasarkan role.

src\app\dashboard\layout.tsx
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
import Main from "./components/main";
import prisma from "@/lib/prisma";
import { userItems, items } from "./components/menu";
 
export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  const session = await getServerSession(OPTIONS);
 
  // it safe to run this query on the server, don't call it on client side!
  const user = await prisma.user.findUnique({
    where: {
      email: session?.user?.email ?? "",
    },
    select: {
      id: true,
      name: true,
      email: true,
      image: true,
      role: true,
    },
  });
 
  //need to run this on server to make sure no one can change the role on frontend to see admin menu
  const menu = user?.role === "user" ? userItems : items;
 
  return (
    <Main user={user} menu={menu}>
      {children}
    </Main>
  );
}

Selanjutnya agar layout dashboard kita bisa menggunakan menu tadi, passing menu sebagai props pada component Main:

src\app\dashboard\components\main.tsx
"use client";
 
import React, { ReactNode } from "react";
 
import { Layout, theme } from "antd";
 
import { usePathname } from "next/navigation";
 
import Sidebar from "./sidebar";
import { MenuItem } from "./menu";
 
const { Header, Content, Footer } = Layout;
 
type Props = {
  children: ReactNode,
  user: {
    id: string,
    name: string,
    email: string,
    image: string | null,
    role: string,
  } | null,
  menu: MenuItem[],
};
 
const Main: React.FC<Props> = ({ children, user, menu }) => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();
 
  const pathname = usePathname();
 
  return (
    <Layout style={{ minHeight: "100vh" }} hasSider>
      <Sidebar defaultSelectedKeys={[pathname]} items={menu} />
      <Layout>
        <Header style={{ padding: 0, background: colorBgContainer }} />
        <Content style={{ margin: "0 16px" }}>
          <div
            style={{
              padding: 24,
              minHeight: 360,
              background: colorBgContainer,
            }}
          >
            {children}
          </div>
        </Content>
        <Footer style={{ textAlign: "center" }}>
          E-Commerce Cart ©2023 Created by Tahu Coding
        </Footer>
      </Layout>
    </Layout>
  );
};
 
export default Main;

Sekarang coba refresh aplikasinya maka menu untuk role "user" akan tampil dengan benar.

User Dashboard

Meski UI dari page landing masih sederhana, kedepan kita akan merubahnya menjadi tampilan yang lebih menarik, interaktif dan responsive.

Selamat kamu sudah menyelesaikan tutorial kali ini, kedepan kita akan belajar bagaimana cara men-generate data dummy pada database menggunakan faker.

Code untuk tutorial kali ini link repo, jangan lupa star di repo ya!

Stay tune dan jangan lupa share & support Tahu Coding, Terima Kasih!