Featured image of post Create a blog with supabase and nextjs - part 2

Create a blog with supabase and nextjs - part 2

Lets create the UI with react and bootstrap

If you want to jump straight to the source code, here’s the link to the github repository.

Preview of the UI

Here’s what we are going to build:

Home page Login

Create a blog post Read a blog post

The home page

Based on what we’ve done in the previous part, we are now going to create a component to display the posts called PostList.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// --- src/components/PostList.tsx ---
const PostList = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  return (
    <Container>
      <Stack direction="horizontal" gap={3}>
        <h1>Latest posts</h1>
        <Link href={`/posts/new`} legacyBehavior>
          <Button>+ new post</Button>
        </Link>
      </Stack>
      <Row xs={1} md={2}>
        {posts.map((post) => (
          <Col key={post.id}>
            <PostCard post={post} />
          </Col>
        ))}
      </Row>
    </Container>
  );
};
export default PostList;

Nothing complicated, we just display the posts in a grid. We also have a button to create a new post.

Because we are using bootstrap, we need to use the legacyBehavior prop on the Link component to make it work. Now by default next/link creates a a tag with a href attribute. This is not compatible with bootstrap as bootstrap already creates a a tag with the Button component.

More info in the doc here.

Quick note about the types: we are using the Post type from the types/Post.ts file which uses the types from the database. You can generate them by running: npm run dev:gen-types

Now we need a nav bar to display the login button and the title of the blog.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// --- src/components/Header.tsx ---
const Header = () => {
  const user = useUser(); // hook from '@supabase/auth-helpers-react';
  const [showModal, setShowModal] = useState(false);
  const handleLogout = () => {
    // logout logic will go here
  };
  return (
    <>
      <SignupModal show={showModal} handleClose={() => setShowModal(false)} />
      <Navbar bg="light">
        <Container>
          <Navbar.Brand href="#home">My Supabase blog</Navbar.Brand>
          <Navbar.Toggle />
          <Navbar.Collapse className="justify-content-end">
            {user ? (
              <Button variant="link" onClick={handleLogout}>
                Logout
              </Button>
            ) : (
              <Button onClick={() => setShowModal(true)}>Login</Button>
            )}
          </Navbar.Collapse>
        </Container>
      </Navbar>
    </>
  );
};
export default Header;

Those components will be used in the index.tsx file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// --- src/pages/index.tsx ---
const Home = () => {
  return (
    <>
      {/* see previous step */}
      <main>
        <Header />
        <PostList />
      </main>
    </>
  );
};

The login modal 🔒

We are going to use the Modal component from bootstrap to display the login form so we don’t need to create a separate page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// --- src/components/SignupModal.tsx ---
const SignupModal = ({ show = false, handleClose }: Props) => {
  const [email, setEmail] = useState('');

  const handleSubmit = async (evt: FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
    // login logic will go here
  };

  return (
    <Modal show={show} onHide={handleClose}>
      <Card body>
        <Form id="login" onSubmit={handleSubmit}>
          <Form.Group className="mb-3" controlId="formLoginEmail">
            <Form.Label>Email address</Form.Label>
            <Form.Control
              type="email"
              value={email}
              onChange={({ target }) => setEmail(target.value)}
            />
          </Form.Group>
          <Button type="submit" disabled={loading}>
            Login
          </Button>
        </Form>
      </Card>
    </Modal>
  );
};

export default SignupModal;

Post creation page

For this new page (/posts/new), we are going to keep the logic simple and in the page, the form will be a separated component called PostEditForm.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// --- src/pages/posts/new.tsx ---
const NewPostPage = () => {
  const handleSubmit = async (title: string, body: string) => {
    // create post logic will go here
  };

  return (
    <>
      <main>
        <Header />
        <Container>
          <Link href="/" legacyBehavior>
            <Button variant="link">{'<'}Back to all posts</Button>
          </Link>
          <h1>New Post</h1>
          <PostEditForm saveForm={handleSubmit} />
        </Container>
      </main>
    </>
  );
};

export default NewPostPage;

The PostEditForm component is pretty simple, it just displays a form with one input and a submit button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// --- src/components/PostEditForm.tsx ---
const PostEditForm = ({ saveForm, post }: Props) => {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');

  // if we are editing a post, we need to set the initial values on mount
  useEffect(() => {
    if (post) {
      setTitle(post.title);
      setBody(post.body);
    }
  }, [post]);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    await saveForm(title, body);
  };

  return (
    <Form onSubmit={handleSubmit}>
      <Form.Group className="mb-3">
        <Form.Label>Title:</Form.Label>
        <Form.Control
          value={title}
          onChange={({ target }) => setTitle(target.value)}
        />
      </Form.Group>
      <Form.Group className="mb-3">
        <Form.Label>Content:</Form.Label>
        <Form.Control
          as="textarea"
          rows={3}
          value={body}
          onChange={({ target }) => setBody(target.value)}
        />
      </Form.Group>
      <Button type="submit">Save</Button>
    </Form>
  );
};

export default PostEditForm;

The post page and the edit page 📝

The post page will display the post and a button to edit it. The edit component will be the same as the new post component but with the initial values set.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// --- src/pages/posts/[id].tsx ---
const PostPage = ({ post }: Props) => {
  const [isEditing, setIsEditing] = useState(false);

  const handleSubmit = async (title: string, body: string) => {
    // update post logic will go here
  };

  return (
    <main>
      <Header />
      <Container>
        <Link href="/" legacyBehavior>
          <Button variant="link">{'<'}Back to all posts</Button>
        </Link>
        {isEditing ? (
          <PostEditForm post={post} saveForm={handleSubmit} />
        ) : (
          <>
            <h1>{post.title}</h1>
            <div>{post.body}</div>
            {user && user.id === post.author && (
              <Button onClick={() => setIsEditing(true)}>Edit post</Button>
            )}
          </>
        )}
      </Container>
    </main>
  );
};

The UI is now done, in the next part we will create the hooks to fetch the data from the database.

Next step the hooks 🎣