DEV Community

Cover image for Let's create a React File Manager Chapter XIV: File Manager Body
Hasan Zohdy
Hasan Zohdy

Posted on

Let's create a React File Manager Chapter XIV: File Manager Body

So we've done a lot of work in the previous chapters in the sidebar and loaders, and now it's time to put some efforts in the file manager body.

Little update in progress bar loader

We'll just display a white background if the progress is 0.

// LoadingProgressBar.tsx
...
  return (
    <Progress
      size="lg"
      value={progress}
      striped
    // 👇🏻  We'll add the styles prop to style the root
      styles={{
        root: {
          backgroundColor: progress === 0 ? "white" : undefined,
        },
      }}
      label={progress > 0 ? `${progress}%` : undefined}
      color={mapProgressColor()}
      animate
    />
  );
Enter fullscreen mode Exit fullscreen mode

Separating The Overlay in its own component

As we know now the overlay appears on the file manager loading state, so we'll create a separate component to be only rendered when the loading happen.

So let's create a new file Content/ContentOverlay.tsx and add the following code:

// ContentOverlay.tsx
import { LoadingOverlay } from "@mantine/core";
import { useLoading } from "app/file-manager/hooks";

export default function ContentOverlay() {
  const isLoading = useLoading();
  return <LoadingOverlay visible={isLoading} overlayBlur={2} />;
}
Enter fullscreen mode Exit fullscreen mode

Now let's import it in Content.tsx.

// Content.tsx
import { Card } from "@mantine/core";
import { ContentWrapper } from "./Content.styles";
import ContentOverlay from "./ContentOverlay"; 👈🏻

export default function Content() {
  return (
    <>
      <Card shadow="sm">
        <ContentWrapper>
          <ContentOverlay /> 👈🏻
        </ContentWrapper>
      </Card>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nodes List

Now its time for the big part, the nodes list, so let's create a new file Content/NodesList.tsx That will render all nodes.

But we need to split nodes into two arrays, one for folders and one for files.

// NodesList.tsx
import { Grid } from "@mantine/core";
import { useKernel } from "app/file-manager/hooks";
import { useMemo } from "react";
import { DirectoryNode, FileNode } from "./ContentNode";

export default function NodesList() {
  const kernel = useKernel();

  const currentDirectoryNode = kernel.currentDirectoryNode;

  const [directories, files] = useMemo(() => {
    const node = currentDirectoryNode;

    if (!node || !node.children?.length) return [[], []];

    return [
      node.children.filter(node => node.isDirectory),
      node.children.filter(node => !node.isDirectory),
    ];
  }, [currentDirectoryNode]);

  return (
    <>
      <Grid>
        {directories.map(node => (
          <Grid.Col key={node.path} span={2}>
          {node.name}
          </Grid.Col>
        ))}
        {files.map(node => (
          <Grid.Col key={node.path} span={2}>
          {node.name}
          </Grid.Col>
        ))}
      </Grid>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

We created a memo to list all directories nodes and files nodes as well, but we want to make sure first there is current directory node to be loaded and it has children as well.

Now you should see something like this:

File Manager

Directory Nodes and File Nodes

So we need to make full control over each node, either in the directory node list or in the file node list, so we'll create two components DirectoryNode and FileNode to handle each node, they will be created in Content/ContentNode directory.

// Content/ContentNode/DirectoryNode.tsx

import { DirectoryNodeProps } from "./ContentNode.types";

export default function DirectoryNode({ node }: DirectoryNodeProps) {
    return <>{node.name}</>;
}
Enter fullscreen mode Exit fullscreen mode
// Content/ContentNode/FileNode.tsx

import { FileNodeProps } from "./ContentNode.types";

export default function FileNode({ node }: FileNodeProps) {
    return <>{node.name}</>;
}
Enter fullscreen mode Exit fullscreen mode

Let's create ContentNode.types.ts to hold the types for both components.

// Content/ContentNode/ContentNode.types.ts
import { Node } from "app/file-manager/Kernel";

export type FileNodeProps = {
  node: Node;
};

export type DirectoryNodeProps = {
  node: Node;
};
Enter fullscreen mode Exit fullscreen mode

All what we added in the props is just the node object.

Now let's create an index file to export all components in the directory.

// Content/ContentNode/index.ts

export { default as DirectoryNode } from "./DirectoryNode";
export { default as FileNode } from "./FileNode";
Enter fullscreen mode Exit fullscreen mode

Now let's import these nodes in our NodesList

// NodesList.tsx
import { Grid } from "@mantine/core";
import { useKernel } from "app/file-manager/hooks";
import { useMemo } from "react";
👉🏻 import { DirectoryNode, FileNode } from "./ContentNode";

export default function NodesList() {
  const kernel = useKernel();

  const currentDirectoryNode = kernel.currentDirectoryNode;

  const [directories, files] = useMemo(() => {
    const node = currentDirectoryNode;

    if (!node || !node.children?.length) return [[], []];

    return [
      node.children.filter(node => node.isDirectory),
      node.children.filter(node => !node.isDirectory),
    ];
  }, [currentDirectoryNode]);

  return (
    <>
      <Grid>
        {directories.map(node => (
          <Grid.Col key={node.path} span={2}>
👉🏻            <DirectoryNode node={node} />
          </Grid.Col>
        ))}
        {files.map(node => (
          <Grid.Col key={node.path} span={2}>
👉🏻            <FileNode node={node} />
          </Grid.Col>
        ))}
      </Grid>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useCurrentDirectoryNode Hook

Let's create a new hook that returns current directory node, and also listen for any change for it, if so then re-render any component that uses this hook.

// hooks/useCurrentDirectoryNode.ts

import { Node } from "app/file-manager/Kernel";
import { useEffect, useState } from "react";
import useKernel from "./useKernel";

export default function useCurrentDirectoryNode() {
  const kernel = useKernel();
  const [node, setNode] = useState<Node | undefined>(
    kernel.currentDirectoryNode,
  );

  useEffect(() => {
    const event = kernel.on("directoryChange", setNode);

    return () => event.unsubscribe();
  }, [kernel]);

  return node;
}
Enter fullscreen mode Exit fullscreen mode

We simply get the current directory node from the kernel and set it in a state then we watched for any change that happen to current node change, in that case we'll update the state thus the component will be re-rendered.

Using useEvent

There is another good hook to use with Events, useEvent that will handle the event unsubscribe for you, so you don't need to worry about it.

// hooks/useCurrentDirectoryNode.ts

👉🏻 import { useEvent } from "@mongez/react";
import { Node } from "app/file-manager/Kernel";
import { useState } from "react";
import useKernel from "./useKernel";

export default function useCurrentDirectoryNode() {
  const kernel = useKernel();
  const [node, setNode] = useState<Node | undefined>(
    kernel.currentDirectoryNode,
  );

👉🏻   useEvent(() => kernel.on("directoryChange", setNode));

  return node;
}
Enter fullscreen mode Exit fullscreen mode

This will do exactly the same effect as the previous code, but it's more readable and you don't need to worry about the unsubscribe event.

Note that the useEvent callback must return the Event Subscription.

Now let's update our NodesList to listen for changes in the current directory node so it makes a new render.

// NodesList.tsx
import { Grid } from "@mantine/core";
// 👇🏻 import the hook
import { useCurrentDirectoryNode } from "app/file-manager/hooks";
import { useMemo } from "react";
import { DirectoryNode, FileNode } from "./ContentNode";

export default function NodesList() {
   // 👇🏻 remove the useKernel hook and replace it with useCurrentDirectoryNode

   const kernel = useKernel();

   const currentDirectoryNode = kernel.currentDirectoryNode;

   const currentDirectoryNode = useCurrentDirectoryNode();

  const [directories, files] = useMemo(() => {
    const node = currentDirectoryNode;

    if (!node || !node.children?.length) return [[], []];

    return [
      node.children.filter(node => node.isDirectory),
      node.children.filter(node => !node.isDirectory),
    ];
  }, [currentDirectoryNode]);

  return (
    <>
      <Grid>
        {directories.map(node => (
          <Grid.Col key={node.path} span={2}>
            <DirectoryNode node={node} />
          </Grid.Col>
        ))}
        {files.map(node => (
          <Grid.Col key={node.path} span={2}>
            <FileNode node={node} />
          </Grid.Col>
        ))}
      </Grid>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Enhancing UI for DirectoryNode and FileNode

Now let's make some nice UI for each node, we'll use NavLink component from Mantine and also some icons as well.

// DirectoryNode.tsx
import { NavLink, useMantineTheme } from "@mantine/core";
import { IconFolder } from "@tabler/icons";
import { useKernel } from "app/file-manager/hooks";
import { DirectoryNodeProps } from "./ContentNode.types";

export default function DirectoryNode({ node }: DirectoryNodeProps) {
    // 👇🏻 get the theme
  const theme = useMantineTheme();
  const kernel = useKernel();

  return (
    // 👇🏻 use NavLink component
    <NavLink
      style={{
        // 👇🏻 center the node and make the cursor to be default
        textAlign: "center",
        cursor: "default",
      }}
      // 👇🏻 add the icon, we'll use the IconFolder and give it some good styles from the theme
      label={
        <>        
          <IconFolder
            fill={theme.colors.blue[4]}
            strokeWidth={1.5}
            color={theme.colors.blue[9]}
            size={40}
          />
          <div>{node.name}</div>
        </>
      }
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's do the same in the FileNode component.

// FileNode.tsx
import { NavLink, useMantineTheme } from "@mantine/core";
import { IconFileInfo as Icon } from "@tabler/icons";
import { FileNodeProps } from "./ContentNode.types";

export default function FileNode({ node }: FileNodeProps) {
  const theme = useMantineTheme();
  return (
    <NavLink
      style={{
        textAlign: "center",
        cursor: "default",
      }}
      label={
        <>
          <Icon
            fill={theme.colors.green[4]}
            strokeWidth={1.5}
            color={theme.colors.green[9]}
            size={40}
          />
          <div>{node.name}</div>
        </>
      }
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Now the final ui will look like this:

File Manager

Open Directory on Double Click

Let's add a little feature, when the user double click on the Directory node, let's open it.

// DirectoryNode.tsx
import { NavLink, useMantineTheme } from "@mantine/core";
import { IconFolder } from "@tabler/icons";
import { useKernel } from "app/file-manager/hooks";
import { DirectoryNodeProps } from "./ContentNode.types";

export default function DirectoryNode({ node }: DirectoryNodeProps) {
  const theme = useMantineTheme();
  const kernel = useKernel();

  return (
    <NavLink
      style={{
        textAlign: "center",
        cursor: "default",
      }}
      // 👇🏻 add the onDoubleClick event
      onDoubleClick={() => kernel.load(node.path)}
      label={
        <>
          <IconFolder
            fill={theme.colors.blue[4]}
            strokeWidth={1.5}
            color={theme.colors.blue[9]}
            size={40}
          />
          <div>{node.name}</div>
        </>
      }
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Article Repository

You can see chapter files in Github Repository

Don't forget the main branch has the latest updated code.

Tell me where you are now

If you're following up with me this series, tell me where are you now and what you're struggling with, i'll try to help you as much as i can.

Salam.

Top comments (0)