Repository: kondraschovAV1999/study-helper
Pull request #85: Create a dialog component for new folder
Author: login: VachanaBK
Created: 2025-04-18T03:13:50Z | Merged: 2025-04-18T21:48:42Z

Use Case 16 - Create folder


Diff:
diff --git a/app/actions.ts b/app/actions.ts
index 18d102d..d113e62 100644
--- a/app/actions.ts
+++ b/app/actions.ts
@@ -167,3 +167,110 @@ export const signOutAction = async () => {
   await supabase.auth.signOut();
   return redirect("/login");
 };
+
+export async function createFolder(
+  folder_name: string,
+  parent_id: string = "00000000-0000-0000-0000-000000000000"
+) {
+  const supabase = await createClient();
+  if (parent_id === "00000000-0000-0000-0000-000000000000") {
+    const { data, error: err } = await supabase.rpc(
+      "get_folder_id_under_root",
+      {
+        target_folder_name: "My Folders",
+      }
+    );
+    if (err) {
+      return {
+        success: false,
+        message: err.message,
+      };
+    }
+    parent_id = data;
+  }
+
+  const { data, error } = await supabase.rpc("create_folder", {
+    folder_name,
+    parent_id,
+  });
+
+  if (error) {
+    console.error("Folder creation error: ", error);
+    return {
+      success: false,
+      message: error.message,
+    };
+  }
+
+  return {
+    success: true,
+    message: "Folder was successfully created",
+  };
+}
+
+export interface Folder {
+  id: string;
+}
+
+export interface FolderInFolder {
+  folder_id: string;
+  folders: Folder;
+  folder_name: string;
+}
+
+export async function fetchSubFolders(
+  folder_name: string,
+  parent_id: string = "00000000-0000-0000-0000-000000000000"
+): Promise<{
+  success: boolean;
+  message: string;
+  content: FolderInFolder[];
+}> {
+  const supabase = await createClient();
+  const { data: user_id, error: err_fetching_user } = await supabase.rpc(
+    "fetch_current_user_id"
+  );
+  if (err_fetching_user) {
+    return {
+      success: false,
+      message: err_fetching_user.message,
+      content: [],
+    };
+  }
+
+  if (parent_id === "00000000-0000-0000-0000-000000000000") {
+    const { data, error: err } = await supabase.rpc(
+      "get_folder_id_under_root",
+      {
+        target_folder_name: "My Folders",
+      }
+    );
+    if (err) {
+      return {
+        success: false,
+        content: [],
+        message: err.message,
+      };
+    }
+    parent_id = data;
+  }
+
+  const { data, error } = await supabase
+    .from("folder_in_folder")
+    .select("*")
+    .eq("user_id", user_id)
+    .eq("parent_id", parent_id);
+
+  if (error) {
+    return {
+      success: false,
+      content: [],
+      message: error.message,
+    };
+  }
+  return {
+    success: true,
+    message: "",
+    content: data as FolderInFolder[],
+  };
+}
diff --git a/components/create-folder-dialog.tsx b/components/create-folder-dialog.tsx
new file mode 100644
index 0000000..f0d2486
--- /dev/null
+++ b/components/create-folder-dialog.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+
+interface CreateFolderDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  onSubmit: (
+    folder_name: string,
+    parent_id?: string
+  ) => Promise<{
+    success: boolean;
+    message: string;
+  }>;
+}
+
+export function CreateFolderDialog({
+  open,
+  onOpenChange,
+  onSubmit,
+}: CreateFolderDialogProps) {
+  const [folderName, setFolderName] = useState("");
+  const [error, setError] = useState<string | null>(null);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    const trimmedName = folderName.trim();
+    setError(null);
+    const result = await onSubmit(trimmedName);
+    if (result.success) {
+      setFolderName("");
+      onOpenChange(false);
+    } else {
+      setError(result.message || "Failed to create folder");
+    }
+  };
+
+  return (
+    <Dialog
+      open={open}
+      onOpenChange={(open) => {
+        if (!open) {
+          setError(null);
+          setFolderName("");
+        }
+        onOpenChange(open);
+      }}
+    >
+      <DialogContent>
+        <DialogHeader>
+          <DialogTitle>Create New Folder</DialogTitle>
+          <DialogDescription>
+            Enter a name for your new folder.
+          </DialogDescription>
+        </DialogHeader>
+        <form onSubmit={handleSubmit}>
+          <div className="grid gap-4 py-4">
+            <div className="grid gap-2">
+              <Input
+                id="name"
+                value={folderName}
+                onChange={(e) => {
+                  setFolderName(e.target.value);
+                  setError(null);
+                }}
+                placeholder="Folder name"
+                className={cn(
+                  error && "border-destructive focus-visible:ring-destructive"
+                )}
+              />
+              {error && <p className="text-sm text-destructive">{error}</p>}
+            </div>
+          </div>
+          <DialogFooter>
+            <Button type="submit" disabled={!folderName.trim()}>
+              Create folder
+            </Button>
+          </DialogFooter>
+        </form>
+      </DialogContent>
+    </Dialog>
+  );
+}
diff --git a/components/create-menu.tsx b/components/create-menu.tsx
index 30ea645..34dd80b 100644
--- a/components/create-menu.tsx
+++ b/components/create-menu.tsx
@@ -46,18 +46,6 @@ const menuItems = [
       icon: PracticeIcon,
     },
   },
-  {
-    component: "action",
-    item: {
-      title: "Folder",
-      action: (isLoggedIn) => {
-        if (!isLoggedIn) {
-          redirect("/login");
-        }
-      },
-      icon: Folder,
-    },
-  },
 ] as MenuItem[];
 
 export default function CreateMenu({ isLoggedIn }: { isLoggedIn: boolean }) {
diff --git a/components/left-sidebar.tsx b/components/left-sidebar.tsx
index 747b164..322ee77 100644
--- a/components/left-sidebar.tsx
+++ b/components/left-sidebar.tsx
@@ -1,10 +1,19 @@
 "use client";
 import { FolderOpen, Home, Plus, Book, Menu } from "lucide-react";
+import { Folder as FolderIcon } from "lucide-react";
 import FlashcardsIcon from "@/components/flashcards-icon";
 import PracticeIcon from "@/components/practice-icon";
 import StudyGuideIcon from "@/components/study-guide-icon";
 import { useSidebar } from "@/components/ui/sidebar";
 import { usePathname } from "next/navigation";
+import { useState, useEffect } from "react";
+import { createClient } from "@/utils/supabase/client";
+import {
+  fetchSubFolders,
+  Folder,
+  FolderInFolder,
+  createFolder,
+} from "../app/actions";
 
 import {
   Sidebar,
@@ -19,6 +28,7 @@ import {
 import NavComponent from "./nav-component";
 import ActionComponent, { ActionItem } from "./action-component";
 import { NavItem } from "./nav-component";
+import { CreateFolderDialog } from "./create-folder-dialog";
 
 export interface NavMenuItem {
   component: "nav";
@@ -43,34 +53,105 @@ export function SidebarMenuGroup({
   sidebarMenu: SidebarMenuProps;
 }) {
   const pathname = usePathname();
+  const [isDialogOpen, setIsDialogOpen] = useState(false);
+  const [subfolders, setSubfolders] = useState<{ id: string; name: string }[]>(
+    []
+  );
+
+  const handleSubmit = async (
+    folder_name: string,
+    parent_id: string = "00000000-0000-0000-0000-000000000000"
+  ) => {
+    const { success, message } = await createFolder(folder_name);
+
+    if (success) {
+      await loadFolders();
+    }
+    return { success, message };
+  };
+
+  const loadFolders = async () => {
+    const { success, message, content } = await fetchSubFolders("My Folders");
+
+    if (!success) {
+      console.error(message);
+      return;
+    }
+
+    if (content) {
+      setSubfolders(
+        content.map((f) => ({
+          id: f.folder_id,
+          name: f.folder_name,
+        }))
+      );
+    }
+  };
+
+  useEffect(() => {
+    loadFolders();
+  }, []);
+
+  const menuItems = [...sidebarMenu.menuItems];
+  if (sidebarMenu.title === "My folders") {
+    subfolders.forEach((folder) => {
+      menuItems.unshift({
+        component: "nav",
+        item: {
+          title: folder.name,
+          href: `/folders/${folder.id}`,
+          icon: FolderIcon,
+        },
+      });
+    });
+  }
 
   return (
-    <SidebarGroup className="!text-xl">
-      {sidebarMenu.title && (
-        <SidebarGroupLabel className= "text-base">{sidebarMenu.title}</SidebarGroupLabel>
-      )}
-      <SidebarGroupContent>
-        <SidebarMenu>
-          {sidebarMenu.menuItems.map((menuItem) => (
-            <SidebarMenuItem key={menuItem.item.title}>
-              <SidebarMenuButton asChild className = "text-base">
-                {menuItem.component === "nav" ? (
-                  <NavComponent
-                    item={menuItem.item as NavItem}
-                    pathname={pathname}
-                  />
-                ) : (
-                  <ActionComponent
-                    item={menuItem.item as ActionItem}
-                    isLoggedIn={true}
-                  />
-                )}
-              </SidebarMenuButton>
-            </SidebarMenuItem>
-          ))}
-        </SidebarMenu>
-      </SidebarGroupContent>
-    </SidebarGroup>
+    <>
+      <SidebarGroup className="!text-xl">
+        {sidebarMenu.title && (
+          <SidebarGroupLabel className="text-base">
+            {sidebarMenu.title}
+          </SidebarGroupLabel>
+        )}
+        <SidebarGroupContent>
+          <SidebarMenu>
+            {menuItems.map((menuItem) => (
+              <SidebarMenuItem key={menuItem.item.title}>
+                <SidebarMenuButton asChild className="text-base">
+                  {menuItem.component === "nav" ? (
+                    <NavComponent
+                      item={menuItem.item as NavItem}
+                      pathname={pathname}
+                    />
+                  ) : (
+                    <ActionComponent
+                      item={{
+                        ...(menuItem.item as ActionItem),
+                        action: () => {
+                          if (sidebarMenu.title === "My folders") {
+                            setIsDialogOpen(true);
+                          } else {
+                            (menuItem.item as ActionItem).action(true);
+                          }
+                        },
+                      }}
+                      isLoggedIn={true}
+                    />
+                  )}
+                </SidebarMenuButton>
+              </SidebarMenuItem>
+            ))}
+          </SidebarMenu>
+        </SidebarGroupContent>
+      </SidebarGroup>
+
+      <CreateFolderDialog
+        open={isDialogOpen}
+        onOpenChange={setIsDialogOpen}
+        onSubmit={handleSubmit}
+      />
+    </>
   );
 }
 
@@ -89,22 +170,22 @@ const sidebarMenus = {
       {
         component: "nav",
         item: {
-          title: "Your library",
+          title: "My library",
           href: "#",
           icon: FolderOpen,
         },
       },
     ] as MenuItem[],
   },
-  new_folder: {
-    title: "Your folders",
+  folderItems: {
+    title: "My folders",
     menuItems: [
       {
         component: "action",
         item: {
-          title: "New folder",
+          title: "Create New",
           action: () => {},
-          icon: FolderOpen,
+          icon: Plus,
         },
       },
     ] as MenuItem[],
@@ -154,7 +235,7 @@ export default function LeftSidebar() {
   const renderSidebarMenuButton = () => (
     <SidebarMenuItem>
       <SidebarMenuButton onClick={toggleSidebar}>
-        <Menu className = "!w-6 !h-6"/>
+        <Menu className="!w-6 !h-6" />
       </SidebarMenuButton>
     </SidebarMenuItem>
   );
@@ -173,7 +254,7 @@ export default function LeftSidebar() {
 
           <SidebarMenuGroup sidebarMenu={sidebarMenus.navigation} />
           <Divider />
-          <SidebarMenuGroup sidebarMenu={sidebarMenus.new_folder} />
+          <SidebarMenuGroup sidebarMenu={sidebarMenus.folderItems} />
           <Divider />
           <SidebarMenuGroup sidebarMenu={sidebarMenus.learning} />
         </Sidebar>
@@ -181,7 +262,7 @@ export default function LeftSidebar() {
 
       <main className="flex-1 p-4">
         <button className="md:hidden mb-4" onClick={toggleSidebar}>
-          <Menu size = {34}/>
+          <Menu size={34} />
         </button>
       </main>
     </div>
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000..7203b5c
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Overlay>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+  <DialogPrimitive.Overlay
+    ref={ref}
+    className={cn(
+      "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+      className
+    )}
+    {...props}
+  />
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
+>(({ className, children, ...props }, ref) => (
+  <DialogPortal>
+    <DialogOverlay />
+    <DialogPrimitive.Content
+      ref={ref}
+      className={cn(
+        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
+        <X className="h-4 w-4" />
+        <span className="sr-only">Close</span>
+      </DialogPrimitive.Close>
+    </DialogPrimitive.Content>
+  </DialogPortal>
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col space-y-1.5 text-center sm:text-left",
+      className
+    )}
+    {...props}
+  />
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
+      className
+    )}
+    {...props}
+  />
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Title>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
+>(({ className, ...props }, ref) => (
+  <DialogPrimitive.Title
+    ref={ref}
+    className={cn(
+      "text-lg font-semibold leading-none tracking-tight",
+      className
+    )}
+    {...props}
+  />
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+  React.ElementRef<typeof DialogPrimitive.Description>,
+  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
+>(({ className, ...props }, ref) => (
+  <DialogPrimitive.Description
+    ref={ref}
+    className={cn("text-sm text-muted-foreground", className)}
+    {...props}
+  />
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+  Dialog,
+  DialogPortal,
+  DialogOverlay,
+  DialogClose,
+  DialogTrigger,
+  DialogContent,
+  DialogHeader,
+  DialogFooter,
+  DialogTitle,
+  DialogDescription,
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index fd56228..03f4aaf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,8 @@
         "next-themes": "^0.4.3",
         "prettier": "^3.3.3",
         "react": "19.0.0",
-        "react-dom": "19.0.0"
+        "react-dom": "19.0.0",
+        "uuid": "11.1.0"
       },
       "devDependencies": {
         "@types/node": "22.10.2",
@@ -4025,6 +4026,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/uuid": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+      "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/esm/bin/uuid"
+      }
+    },
     "node_modules/webidl-conversions": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/package.json b/package.json
index 574313f..75c1f51 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
     "next-themes": "^0.4.3",
     "prettier": "^3.3.3",
     "react": "19.0.0",
-    "react-dom": "19.0.0"
+    "react-dom": "19.0.0",
+    "uuid": "11.1.0"
   },
   "devDependencies": {
     "@types/node": "22.10.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3645fdb..7982031 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -68,6 +68,9 @@ importers:
       react-dom:
         specifier: 19.0.0
         version: 19.0.0(react@19.0.0)
+      uuid:
+        specifier: 11.1.0
+        version: 11.1.0
     devDependencies:
       '@types/node':
         specifier: 22.10.2
@@ -1574,6 +1577,10 @@ packages:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  uuid@11.1.0:
+    resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+    hasBin: true
+
   webidl-conversions@3.0.1:
     resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
 
@@ -3006,6 +3013,8 @@ snapshots:
 
   util-deprecate@1.0.2: {}
 
+  uuid@11.1.0: {}
+
   webidl-conversions@3.0.1: {}
 
   whatwg-url@5.0.0:
diff --git a/utils/get-folder-id-under-root.ts b/utils/get-folder-id-under-root.ts
new file mode 100644
index 0000000..e69de29


Reviews:
user: login: kondraschovAV1999
body: Good job! I'll finish the changes myself.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-18T16:54:58Z
user: login: kmdingman1
body: LGTM, thanks guys!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-18T21:46:29Z
user: login: kondraschovAV1999
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-18T21:48:31Z

Review comments:
diff_hunk: @@ -0,0 +1,183 @@
+"use client";
path: components/create-folder-dialog.tsx
user: login: kondraschovAV1999
body: I think this should be in component/ui folder. Whenever you use shadcn component just use pnpm command, don't copy code manually. For this case use 
```bash
pnpm dlx shadcn@latest add dialog
```

created_at: 2025-04-18T16:50:20Z
author_association: OWNER
diff_hunk: @@ -37,40 +40,162 @@ interface SidebarMenuProps {
   menuItems: MenuItem[];
 }
 
+interface Folder {
+  id: string;
+}
+
+interface FolderInFolder {
+  folder_id: string;
+  folders: Folder;
+  folder_name: string;
+}
+
 export function SidebarMenuGroup({
   sidebarMenu,
 }: {
   sidebarMenu: SidebarMenuProps;
 }) {
   const pathname = usePathname();
+  const [isDialogOpen, setIsDialogOpen] = useState(false);
path: components/left-sidebar.tsx
user: login: kondraschovAV1999
body: I would suggest to use server action and also probably we would need to use rpc from supabase when we update more than one table in database to achieve transactions. I'll do necessary changes. 
created_at: 2025-04-18T16:54:25Z
author_association: OWNER

PR comments:
user: login: VachanaBK
created_at: 2025-04-18T17:49:31Z
body: Added new shadcn dialog component. basically, shadcn uses radix ui under the hood. I have added in ui components for reuse though.
author_association: COLLABORATOR
user: login: kondraschovAV1999
created_at: 2025-04-18T21:25:17Z
body: Made some changes.
I've created a function in supabase. It's a util function. This function retrieves user's id from our custom table. I had many bugs because I used auth.uid() before. 
### fetch_current_user_id()
```sql

DECLARE
  result UUID;
BEGIN
  SELECT id INTO result
  FROM "user" AS u
  WHERE u.auth_user_id = auth.uid(); 

  RETURN result;
END;
```

I also have created a util function to create folders in a transaction.
### create_folder(parent_id uuid, folder_name text)
```sql
DECLARE
  folder_id UUID;
  user_id UUID;
  result folder_in_folder;
BEGIN
  -- Insert new folder and get its ID
  INSERT INTO folder (created_at, updated_at)
  VALUES (now(), now())
  RETURNING id INTO folder_id;

  -- Get current user ID
  SELECT id INTO user_id
  FROM "user" AS u
  WHERE u.auth_user_id = auth.uid();

  -- If user not found, raise error
  IF user_id IS NULL THEN
    RAISE EXCEPTION 'Cannot find user';
  END IF;

  -- Default parent_id if not provided
  IF parent_id IS NULL THEN
    parent_id := '00000000-0000-0000-0000-000000000000';
  END IF;

  -- Insert into folder_in_folder and return the result
  INSERT INTO folder_in_folder (user_id, parent_id, folder_name, folder_id)
  VALUES (user_id, parent_id, folder_name, folder_id)
  RETURNING * INTO result;

  RETURN result;
END;
```

I also have updated the get_folder_id_under_root function because it was in correct.
```sql

declare
  root_folder_id uuid;
  folder_id uuid;
  curr_user_id uuid;
begin
  -- Get Root folder ID for current user
  curr_user_id := fetch_current_user_id();
  select f.folder_id into root_folder_id
  from folder_in_folder as f
  where f.user_id = curr_user_id and f.folder_name = 'Root';

  if root_folder_id is null then
    raise exception 'Root folder not found for user %', auth.uid();
  end if;

  -- Get the target folder under Root
  select f.folder_id into folder_id
  from folder_in_folder as f
  where f.user_id = curr_user_id
    and f.folder_name = target_folder_name
    and f.parent_id = root_folder_id;

  if folder_id is null then
    raise exception '% folder not found under Root for user %', target_folder_name, auth.uid();
  end if;

  return folder_id;
end;

```
author_association: OWNER