📲 Post List

Build the UI for the feed of all posts

AllPosts.tsx:

import { useContext, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { UserContext } from "./App";
import { castVote } from "./cast-vote";
import { CreatePost } from "./CreatePost";
import { GetPostsResponse } from "./database.types";
import { supaClient } from "./supa-client";
import { timeAgo } from "./time-ago";
import { UpVote } from "./UpVote";
import { usePostScore } from "./use-post-score";

export function AllPosts() {
  const { session } = useContext(UserContext);
  const { pageNumber } = useParams();
  const [bumper, setBumper] = useState(0);
  const [posts, setPosts] = useState<GetPostsResponse[]>([]);
  const [myVotes, setMyVotes] = useState<
    Record<string, "up" | "down" | undefined>
  >({});
  useEffect(() => {
    const queryPageNumber = pageNumber ? +pageNumber : 1;
    supaClient
      .rpc("get_posts", { page_number: queryPageNumber })
      .select("*")
      .then(({ data }) => {
        setPosts(data as GetPostsResponse[]);
        if (session?.user) {
          supaClient
            .from("post_votes")
            .select("*")
            .eq("user_id", session.user.id)
            .then(({ data: votesData }) => {
              if (!votesData) {
                return;
              }
              const votes = votesData.reduce((acc, vote) => {
                acc[vote.post_id] = vote.vote_type as any;
                return acc;
              }, {} as Record<string, "up" | "down" | undefined>);
              setMyVotes(votes);
            });
        }
      });
  }, [session, bumper, pageNumber]);

  return (
    <>
      {session && (
        <CreatePost
          newPostCreated={() => {
            setBumper(bumper + 1);
          }}
        />
      )}
      <div className="posts-container">
        {posts?.map((post, i) => (
          <Post
            key={post.id}
            postData={post}
            myVote={myVotes?.[post.id] || undefined}
            onVoteSuccess={() => {
              setBumper(bumper + 1);
            }}
          />
        ))}
      </div>
    </>
  );
}

function Post({
  postData,
  myVote,
  onVoteSuccess,
}: {
  postData: GetPostsResponse;
  myVote: "up" | "down" | undefined;
  onVoteSuccess: () => void;
}) {
  const score = usePostScore(postData.id, postData.score);
  const { session } = useContext(UserContext);
  return (
    <div className="post-container">
      <div className="post-upvote-container">
        <UpVote
          direction="up"
          filled={myVote === "up"}
          enabled={!!session}
          onClick={async () => {
            await castVote({
              postId: postData.id,
              userId: session?.user.id as string,
              voteType: "up",
              onSuccess: () => {
                onVoteSuccess();
              },
            });
          }}
        />
        <p className="text-center" data-e2e="upvote-count">
          {score}
        </p>
        <UpVote
          direction="down"
          filled={myVote === "down"}
          enabled={!!session}
          onClick={async () => {
            await castVote({
              postId: postData.id,
              userId: session?.user.id as string,
              voteType: "down",
              onSuccess: () => {
                onVoteSuccess();
              },
            });
          }}
        />
      </div>
      <Link to={`/post/${postData.id}`} className="flex-auto">
        <p className="mt-4">
          Posted By {postData.username} {timeAgo((postData as any).created_at)}{" "}
          ago
        </p>
        <h3 className="text-2xl">{postData.title}</h3>
      </Link>
    </div>
  );
}

Form component to create a New post, CreatePost.tsx:

import { Session } from "@supabase/supabase-js";
import { useContext, useRef, useState } from "react";
import { UserContext } from "./App";
import { supaClient } from "./supa-client";

export interface CreatePostProps {
  newPostCreated?: () => void;
}

function createNewPost({
  session,
  title,
  content,
}: {
  session: Session | null;
  title: string;
  content: string;
}) {
  return supaClient.rpc("create_new_post", {
    userId: session?.user.id || "",
    title,
    content,
  });
}

export function CreatePost({ newPostCreated = () => {} }: CreatePostProps) {
  const { session } = useContext(UserContext);
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const titleInputRef = useRef<HTMLInputElement>(null);
  const contentInputRef = useRef<HTMLTextAreaElement>(null);
  return (
    <>
      <form
        className="create-post-form"
        data-e2e="create-post-form"
        onSubmit={(event) => {
          event.preventDefault();
          createNewPost({ session, title, content }).then(({ error }) => {
            if (error) {
              console.log(error);
            } else {
              setTitle("");
              setContent("");
              if (titleInputRef.current) {
                titleInputRef.current.value = "";
              }
              if (contentInputRef.current) {
                contentInputRef.current.value = "";
              }
              newPostCreated();
            }
          });
        }}
      >
        <h3>Create A New Post</h3>
        <input
          type="text"
          name="title"
          ref={titleInputRef}
          className="create-post-title-input"
          placeholder="Your Title Here"
          onChange={({ target: { value } }) => {
            setTitle(value);
          }}
        />
        <textarea
          name="contents"
          ref={contentInputRef}
          placeholder="Your content here"
          className="create-post-content-input"
          onChange={({ target: { value } }) => {
            setContent(value);
          }}
        />
        <div>
          <button type="submit" className="create-post-submit-button">
            Submit
          </button>
        </div>
      </form>
    </>
  );
}

Questions? Let's chat

Open Discord