Layout
For the base layout, it’s going to be like this:
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:
And this is using 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>
)
}
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>
)
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>
)
}
Here’s the difference between the dropdown in Navbar, Tags, and TipTap Menu Bar:
- How to manage the dropdown
Navbar and Tags
const [open, setOpen] = useState(false);
TipTap Menu Bar
const [activeDropdown, setActiveDropdown] = useState<"text" | "list" | "clear" | null>(null);
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.
- Scalable toggle logic
Before:
setOpen(!open)
After:
const toggleDropdown = (name) => {
setActiveDropdown(activeDropdown === name ? null : name);
};
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.
- Close after action
const handleAction = (callback: () => void) => {
callback();
setActiveDropdown(null);
};
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>
)
}
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>
)
}
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>
</>
);
}
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>
);
}
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>
)
}



Top comments (0)