DEV Community

Nabilla Trisnani
Nabilla Trisnani

Posted on

Step 2: Slicing

Layout

For the base layout, it’s going to be like this:

Wireframe

Here’s the detail:

Navbar:

  • Title (All Notes/Archived Notes) and its total
  • Searchbar
  • Theme switcher
  • Setting button

Sidebar:

  • Section 1:
    • Logo
    • Button to show all notes
    • Button to show archived notes
  • Section 2:
    • List of all tags

Note List:

  • Button to add new note
  • List of all notes or archived notes

Detail:

  • Title
  • Tags
  • Last Edited Date
  • Textarea for the note
  • Footer for button submit and cancel

Action:

  • Button delete
  • Button archive

Exited? I know you do. Stick with me.

Preview

Here’s the full preview using light theme:

Light Theme

And this is using dark theme:

Dark Theme

You can checkout my repo for the full code, but we’re going to go through the key parts for the slicing.

Dropdown

I’m going to use dropdown for Navbar, TipTap Menu Bar, and Tags. The concept is to show the dropdown when the trigger is clicked. And when the outside of the dropdown is clicked, the dropdown is closed.

The difference between the dropdown in Navbar and Tags and TipTap Menu Bar is that when I click the content inside TipTap Menu Bar dropdown, it should automatically closed.

Here’s the snippet for the Navbar:

export default function Navbar() {
        //state for dropdown
    const [open, setOpen] = useState(false);

        //useRef to detect click outside
    const ref = useRef<HTMLDivElement>(null);

        //handle click outside
    useEffect(() => {
        function handleClickOutside(e: MouseEvent) {
                //check if the click is from outside or not
            if (ref.current && !ref.current.contains(e.target as Node)) {
                setOpen(false);
            }
        }

                //everytime the click is outside, it runs the handleClickOutside function
        document.addEventListener("mousedown", handleClickOutside);
        return () => document.removeEventListener("mousedown", handleClickOutside);
    }, []);

    return (
            // ref must be ont the parent element
        <div ref={ref} className="flex justify-between items-center py-4 px-6 border-b-[1.5px] border-light-secondary-background dark:border-gray-500">
            ...

            <div className="relative">
                                {/* button trigger */}
                <button
                    onClick={() => setOpen(!open)}
                    className="group/setting hover:cursor-pointer hover:bg-light-highlight hover:text-white py-2 px-3 rounded-lg"
                >
                    <i className="bi bi-gear text-5 text-light-text-primary dark:text-white group-hover/setting:text-white"></i>
                </button>

                {/* show the dropdown when the state "open" is true */}
                {open && (
                    <div className="absolute right-0 border border-[1.5px] border-light-secondary-background dark:border-gray-500 bg-white dark:bg-dark-background rounded-lg p-2">
                        <div className="flex gap-2 items-center">
                            <span className="size-[30px] flex items-center justify-center rounded-full bg-light-highlight text-light-text-primary">N</span>
                            <div>
                                <p className="text-sm truncate text-ellipsis">Nabilla Trisnani</p>
                                <p className="text-xs">nabillatrisnani@gmail.com</p>
                            </div>
                        </div>
                        <hr className="my-3 border-t-[1.5px] border-light-secondary-background dark:border-gray-500" />
                        <button className="bg-red-500 hover:cursor-pointer text-white font-medium px-2 py-1 rounded-lg text-sm min-w-30 w-full">Logout</button>
                    </div>
                )}
            </div>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

For the Tags:

export default function Detail() {
    const ref = useRef<HTMLDivElement>(null);
    const [open, setOpen] = useState(false);
    const [tags, setTags] = useState<Tag[]>([
        { id: 1, name: "Tag 1", color: { id: 1, name: "Red", classBackground: "bg-rose-400 dark:bg-rose-500", textBackground: "text-rose-400 dark:text-rose-500", borderBackground: "border-rose-400 dark:border-rose-500" } },
    ]);

    useEffect(() => {
        function handleClickOutside(e: MouseEvent) {
            if (ref.current && !ref.current.contains(e.target as Node)) {
                setOpen(false);
            }
        }

        document.addEventListener("mousedown", handleClickOutside);
        return () => document.removeEventListener("mousedown", handleClickOutside);
    }, []);

    return (
        <div className="flex w-full">
                ...

            <div ref={ref} className="relative">
                <div
                    onClick={() => setOpen(prev => !prev)}
                    className="h-full w-full"
                >
                    {/* <p className="text-sm text-gray-300">Click here to add tags</p> */}

                    <div className="flex flex-wrap gap-2">
                        {
                            tags.map((tag: Tag, index: number) => {
                                return (
                                    <span key={index}
                                        className={`flex items-center px-1 py-[1px] ${tag.color.classBackground} text-xs text-light-text-primary dark:text-white rounded-sm`}
                                    >
                                        {tag.name}
                                        <i className="bi bi-x-lg ml-2"></i>
                                    </span>
                                )
                            })
                        }
                    </div>
                </div>

                {open && (
                    <div className="absolute z-9 border-[1.5px] border-light-secondary-background dark:border-gray-500 rounded-lg">
                        <div className="flex flex-wrap bg-gray-100 dark:bg-gray-700 rounded-t-lg p-2 gap-2">
                            {
                                tags.map((tag: Tag, index: number) => {
                                    return (
                                        <span key={index}
                                            className={`flex items-center px-1 py-[1px] ${tag.color.classBackground} text-xs text-light-text-primary dark:text-white rounded-sm`}
                                        >
                                            {tag.name}
                                            <i className="bi bi-x-lg ml-2"></i>
                                        </span>
                                    )
                                })
                            }
                            <input type="text" className="text-xs ml-2 outline-0" placeholder="Type Tag Name Here" />
                        </div>
                        <div className="bg-white dark:bg-dark-background py-2 rounded-b-lg">
                            <p className="text-xs mb-2 px-2">Select an option or create a new one</p>
                            {
                                tagsData.map((tag: Tag, index: number) => {
                                    return (
                                        <div key={index} className="py-1 px-2 hover:bg-light-secondary-background/50 dark:hover:bg-dark-secondary-background/10">
                                            <span
                                                className={`block w-max px-1 py-[1px] ${tag.color.classBackground} text-xs text-light-text-primary dark:text-white rounded-sm`}
                                            >
                                                {tag.name}
                                            </span>
                                        </div>
                                    )
                                })
                            }
                        </div>
                    </div>
                )}
            </div>

            ...
        </div>
    )

Enter fullscreen mode Exit fullscreen mode

TipTap Menu Bar

export const MenuBar = ({ editor }: { editor: Editor | null }) => {
    const ref = useRef<HTMLDivElement>(null);
    const [activeDropdown, setActiveDropdown] = useState<"text" | "list" | "clear" | null>(null);

    const toggleDropdown = (name: "text" | "list" | "clear") => {
        setActiveDropdown(activeDropdown === name ? null : name);
    };

    const handleAction = (callback: () => void) => {
        callback();
        setActiveDropdown(null);
    };

    const editorState = useEditorState({
        editor,
        selector: menuBarStateSelector,
    })

    useEffect(() => {
        function handleClickOutside(e: MouseEvent) {
            if (ref.current && !ref.current.contains(e.target as Node)) {
                setActiveDropdown(null)
            }
        }

        document.addEventListener("mousedown", handleClickOutside);
        return () => document.removeEventListener("mousedown", handleClickOutside);
    }, []);

    if (!editor) return null;

    return (
        <div ref={ref} className="control-group text-sm bg-white dark:bg-dark-background">
            <div className="button-group flex gap-2 flex-wrap bg-white dark:bg-dark-background">

                {/* TEXT DROPDOWN */}
                <div className="relative">
                    <div onClick={() => toggleDropdown("text")} className="flex items-center gap-2 cursor-pointer">
                        <i className="text-base bi bi-type"></i>
                        <i className="text-xs bi bi-chevron-down"></i>
                    </div>

                    {activeDropdown === "text" && (
                        <ul className="absolute left-0 z-9 w-max border border-[1.5px] border-light-secondary-background dark:border-gray-500 bg-white dark:bg-dark-background rounded-lg p-2">
                            <li>
                                <button onClick={() => handleAction(() => editor.chain().focus().setParagraph().run())}>
                                    <i className="text-base bi bi-paragraph"></i> Paragraph
                                </button>
                            </li>

                            {[1, 2, 3, 4, 5, 6].map((level) => (
                                <li key={level}>
                                    <button
                                        onClick={() =>
                                            handleAction(() =>
                                                editor.chain().focus().toggleHeading({ level }).run()
                                            )
                                        }
                                        className={editorState?.[`isHeading${level}` as keyof typeof editorState] ? 'is-active' : ''}
                                    >
                                        <i className={`text-base bi bi-type-h${level}`}></i> Heading {level}
                                    </button>
                                </li>
                            ))}
                        </ul>
                    )}
                </div>
            </div>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Here’s the difference between the dropdown in Navbar, Tags, and TipTap Menu Bar:

  1. How to manage the dropdown

Navbar and Tags

const [open, setOpen] = useState(false);
Enter fullscreen mode Exit fullscreen mode

TipTap Menu Bar

const [activeDropdown, setActiveDropdown] = useState<"text" | "list" | "clear" | null>(null);
Enter fullscreen mode Exit fullscreen mode

The state in Navbar and Tags only handle one dropdown whille TipTap Menu Bar handle multiple dropdown. Navbar with Boolean as value, then Enum for TipTap Menu Bar.

  1. Scalable toggle logic

Before:

setOpen(!open)
Enter fullscreen mode Exit fullscreen mode

After:

const toggleDropdown = (name) => {
    setActiveDropdown(activeDropdown === name ? null : name);
};
Enter fullscreen mode Exit fullscreen mode

Meaning:

  • When you click the trigger, it’s going to open the dropdown
  • When you click the trigger again, it’s going to close the dropdown
  • When you click other dropdown trigger, it’s going to close the initial dropdown and open the other one.
  1. Close after action
const handleAction = (callback: () => void) => {
    callback();
    setActiveDropdown(null);
};
Enter fullscreen mode Exit fullscreen mode

This one is the most prominent one. Why? Because in Navbar and Tags, it doesn’t automatically close after you click an action in their dropdown. However, when you click an action from TipTap Menu Bar, the dropdown automatically close.

Toggle Sidebar

For the sidebar toggle, the idea is to hide the sidebar when the button is clicked. The button is in the Navbar and the Sidebar is a different component.

Here’s the snippet for the button in the Navbar, Sidebar, and the page.tsx.

Trigger button

interface NavbarProps {
    toggleShowSidebar: () => void;
}

export default function Navbar({
    toggleShowSidebar,
}: NavbarProps) {

    return (
        <button
            onClick={toggleShowSidebar}
            className="group/sidebar hover:cursor-pointer hover:bg-light-highlight hover:text-white py-2 px-3 rounded-lg"
        >
            <i className="bi bi-layout-sidebar text-5 text-light-text-primary dark:text-white group-hover/sidebar:text-white"></i>
        </button>
    )
}
Enter fullscreen mode Exit fullscreen mode

Sidebar

interface SidebarProps {
    show: boolean;
}

export default function Sidebar({
    show = true,
}: SidebarProps) {
    return (
        <div className={`${show ? "block" : "hidden"} w-[17vw] h-screen p-4 border-r-[1.5px] border-light-secondary-background dark:border-gray-500 shrink-0`}>
            ...
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

page.tsx

export default function Home() {
  const [show, setShow] = useState(true);

  const toggleShowSidebar = () => {
    setShow((prev) => !prev);
  }

  return (
    <>
      <div className="bg-white dark:bg-dark-background min-h-screen flex">
        <Sidebar show={show} />
        <main className="flex-1">
          <Navbar toggleShowSidebar={toggleShowSidebar} />
        </main>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

toggleShowSidebar in Navbar is a prop for function toggleShowSidebar in Home. In Sidebar, it has prop “show” to trigger the class “hidden” and “block”.

Theme Switcher

For the theme switcher, I’m going to use next-themes. I did set it up in the first chapter, but in this chapter, I’m going to improve it. With it, I’m going to create a simple button in Navbar to trigger the theme. When the theme is light, when you click the button, it’s going to change to dark theme, and vice versa. Simple right?

Here’s the snippet in layout.tsx

import "./globals.css";
import { ThemeProvider } from "next-themes";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navbar

export default function Navbar() {
    const { theme, setTheme } = useTheme();

    return (
        <button
            onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
            className="group/theme hover:cursor-pointer hover:bg-light-highlight hover:text-white py-2 px-3 rounded-lg"
        >
            <i className={`bi bi-${theme === "dark" ? "sun" : "moon"} text-5 text-light-text-primary dark:text-white group-hover/theme:text-white`}></i>
        </button>
    )
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)