Command Palette

Search for a command to run...

Building Blocks for the Web

Clean, modern building blocks. Copy and paste into your apps. Works with all React frameworks. Open Source. Free forever.

A post-feed page displaying a list of posts.
post-feed
Files
app/post-feed/page.tsx
"use client";

import * as React from "react";
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
} from "@tanstack/react-query";

import { PostResolver } from "@/components/posts-resolver";
import { POSTS_STREAM } from "@/registry/default/blocks/social-content/post-feed/mock-data";
import { Button } from "@/components/ui/button";
import { FeedCTA } from "@/components/ui/feed-cta";
import {
  InfiniteScroll,
  InfiniteScrollList,
} from "@/components/ui/infinite-scroll";
import { Stack } from "@/components/ui/stack";

const queryClient = new QueryClient();

async function fetchPostsStream(
  limit: number,
  offset: number = 0
): Promise<{ ids: string[]; nextOffset: number }> {
  const startIndex = offset * limit;
  const endIndex = Math.min(startIndex + limit, POSTS_STREAM.length);
  const ids = POSTS_STREAM.slice(startIndex, endIndex).map((post) => post.id);

  return {
    ids,
    nextOffset: endIndex < POSTS_STREAM.length ? offset + 1 : -1,
  };
}

function Posts() {
  const {
    status,
    data,
    error,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ["post-ids"],
    queryFn: (ctx) => fetchPostsStream(10, ctx.pageParam),
    getNextPageParam: (lastGroup) =>
      lastGroup.nextOffset === -1 ? undefined : lastGroup.nextOffset,
    initialPageParam: 0,
  });

  const postIds = data ? data.pages.flatMap((d) => d.ids) : [];

  return (
    <Stack
      spacing={4}
      className="p-4 **:data-[slot=infinite-scroll-container]:h-fit h-screen"
    >
      <FeedCTA
        profileDID="did:pkh:eip155:11155111:0x1a4b3c567890abcdeffedcba1234567890abcdef"
        avatarSrc="https://github.com/akashaorg.png"
        cta="From your mind to the world. "
        onClick={() => {
          console.log("Show beam editor");
        }}
      >
        <Button
          size="sm"
          onClick={() => {
            console.log("Show beam editor");
          }}
        >
          Post Something
        </Button>
      </FeedCTA>
      {status === "pending" ? (
        <p>Loading...</p>
      ) : status === "error" ? (
        <span>Error: {error.message}</span>
      ) : (
        <InfiniteScroll
          count={postIds.length}
          estimatedHeight={220}
          overScan={5}
          gap={16}
          loading={isFetchingNextPage}
          hasNextPage={hasNextPage}
          onLoadMore={() => {
            fetchNextPage();
          }}
          loadingIndicator="Loading ..."
          scrollElementType="element"
        >
          <InfiniteScrollList>
            {(index) => {
              const postId = postIds[index];
              return <PostResolver key={postId} postId={postId} />;
            }}
          </InfiniteScrollList>
        </InfiniteScroll>
      )}
    </Stack>
  );
}

export default function Page() {
  return (
    <QueryClientProvider client={queryClient}>
      <Posts />
    </QueryClientProvider>
  );
}
A post page displaying the main post along with its replies.
post-page
Files
app/post/page.tsx
"use client";

import * as React from "react";
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
} from "@tanstack/react-query";
import { Ellipsis } from "lucide-react";

import { cn } from "@/lib/utils";
import { PostCard } from "@/registry/default/blocks/social-content/post-card";
import {
  POST,
  REPLIES_STREAM,
} from "@/registry/default/blocks/social-content/post-page/mock-data";
import { ReplyEditor } from "@/registry/default/blocks/social-content/reply-editor";
import { ReplyResolver } from "@/registry/default/blocks/social-content/reply-resolver";
import { Card } from "@/components/ui/card";
import {
  InfiniteScroll,
  InfiniteScrollList,
} from "@/components/ui/infinite-scroll";

const queryClient = new QueryClient();

async function fetchRepliesStream(
  limit: number,
  offset: number = 0
): Promise<{ ids: string[]; nextOffset: number }> {
  const startIndex = offset * limit;
  const endIndex = Math.min(startIndex + limit, REPLIES_STREAM.length);
  const ids = REPLIES_STREAM.slice(startIndex, endIndex).map((post) => post.id);

  return {
    ids,
    nextOffset: endIndex < REPLIES_STREAM.length ? offset + 1 : -1,
  };
}
function Post() {
  const { content, ...postProps } = POST;

  const {
    status,
    data,
    error,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ["post-ids"],
    queryFn: (ctx) => fetchRepliesStream(10, ctx.pageParam),
    getNextPageParam: (lastGroup) =>
      lastGroup.nextOffset === -1 ? undefined : lastGroup.nextOffset,
    initialPageParam: 0,
  });

  const replyIds = data ? data.pages.flatMap((d) => d.ids) : [];

  return (
    <div className="p-4 h-full">
      <PostCard
        {...postProps}
        onRepliesClick={() => {
          console.log("Not implemented");
        }}
        className="rounded-b-none"
        menu={{
          trigger: (
            <Ellipsis size={20} className="text-primary hover:text-muted" />
          ),
          items: [
            { content: "Flag", onClick: () => console.log("flag") },
            {
              content: "Delete",
              className: "text-destructive",
              onClick: () => console.log("delete"),
            },
            { content: "Edit", onClick: () => console.log("edit") },
          ],
        }}
      >
        {content}
      </PostCard>
      <Card className="border-y-0 p-2 rounded-none">
        <ReplyEditor
          avatarSrc={"https://github.com/akashaorg.png"}
          onReplyClick={() => {
            console.log("Not implemented");
          }}
        />
      </Card>
      {status === "pending" ? (
        <p>Loading...</p>
      ) : status === "error" ? (
        <span>Error: {error.message}</span>
      ) : (
        <InfiniteScroll
          count={replyIds.length}
          estimatedHeight={220}
          overScan={5}
          gap={0}
          loading={isFetchingNextPage}
          hasNextPage={hasNextPage}
          onLoadMore={() => {
            fetchNextPage();
          }}
          loadingIndicator="Loading ..."
          scrollElementType="window"
        >
          <InfiniteScrollList>
            {(index) => {
              const replyId = replyIds[index];
              return (
                <ReplyResolver
                  key={replyId}
                  replyId={replyId}
                  className={cn(
                    index === replyIds.length - 1
                      ? "rounded-t-none"
                      : "rounded-none border-b-0"
                  )}
                />
              );
            }}
          </InfiniteScrollList>
        </InfiniteScroll>
      )}
    </div>
  );
}

export default function Page() {
  return (
    <QueryClientProvider client={queryClient}>
      <Post />
    </QueryClientProvider>
  );
}
A reply post page with reply editor and replies
reply-page
Files
app/post/page.tsx
"use client";

import * as React from "react";
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
} from "@tanstack/react-query";
import { Ellipsis } from "lucide-react";

import { cn } from "@/lib/utils";
import { ReplyCard } from "@/registry/default/blocks/social-content/reply-card";
import { ReplyEditor } from "@/registry/default/blocks/social-content/reply-editor";
import {
  REPLIES_STREAM,
  REPLY,
} from "@/registry/default/blocks/social-content/reply-page/mock-data";
import { ReplyResolver } from "@/registry/default/blocks/social-content/reply-resolver";
import { Card } from "@/components/ui/card";
import {
  InfiniteScroll,
  InfiniteScrollList,
} from "@/components/ui/infinite-scroll";

const queryClient = new QueryClient();

async function fetchRepliesStream(
  limit: number,
  offset: number = 0
): Promise<{ ids: string[]; nextOffset: number }> {
  const startIndex = offset * limit;
  const endIndex = Math.min(startIndex + limit, REPLIES_STREAM.length);
  const ids = REPLIES_STREAM.slice(startIndex, endIndex).map((post) => post.id);

  return {
    ids,
    nextOffset: endIndex < REPLIES_STREAM.length ? offset + 1 : -1,
  };
}

function Replies() {
  const {
    status,
    data,
    error,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ["post-ids"],
    queryFn: (ctx) => fetchRepliesStream(10, ctx.pageParam),
    getNextPageParam: (lastGroup) =>
      lastGroup.nextOffset === -1 ? undefined : lastGroup.nextOffset,
    initialPageParam: 0,
  });

  const replyIds = data ? data.pages.flatMap((d) => d.ids) : [];

  const { content, ...replyProps } = REPLY;
  return (
    <div className="p-4 h-full">
      <ReplyCard
        onRepliesClick={() => {
          console.log("Not implemented");
        }}
        className="border-b-none rounded-b-none"
        menu={{
          trigger: (
            <Ellipsis size={20} className="text-primary hover:text-muted" />
          ),
          items: [
            { content: "Flag", onClick: () => console.log("flag") },
            {
              content: "Delete",
              className: "text-destructive",
              onClick: () => console.log("delete"),
            },
            { content: "Edit", onClick: () => console.log("edit") },
          ],
        }}
        {...replyProps}
      >
        {content}
      </ReplyCard>
      <Card className="border-y-0 p-2 rounded-none">
        <ReplyEditor
          avatarSrc={"https://github.com/akashaorg.png"}
          onReplyClick={() => {
            console.log("Not implemented");
          }}
        />
      </Card>
      {status === "pending" ? (
        <p>Loading...</p>
      ) : status === "error" ? (
        <span>Error: {error.message}</span>
      ) : (
        <InfiniteScroll
          count={replyIds.length}
          estimatedHeight={220}
          overScan={5}
          gap={0}
          loading={isFetchingNextPage}
          hasNextPage={hasNextPage}
          onLoadMore={() => {
            fetchNextPage();
          }}
          loadingIndicator="Loading ..."
          scrollElementType="window"
        >
          <InfiniteScrollList>
            {(index) => {
              const replyId = replyIds[index];
              return (
                <ReplyResolver
                  key={replyId}
                  replyId={replyId}
                  className={cn(
                    index === replyIds.length - 1
                      ? "rounded-t-none"
                      : "rounded-none border-b-0"
                  )}
                />
              );
            }}
          </InfiniteScrollList>
        </InfiniteScroll>
      )}
    </div>
  );
}

export default function Page() {
  return (
    <QueryClientProvider client={queryClient}>
      <Replies />
    </QueryClientProvider>
  );
}