Zotion

repository·master·Indexed 41 days ago

https://github.com/adityaphasu/notion-clone

A simplified, real-time productivity application clone of Notion featuring hierarchical document organization, a Blocknote-based editor, and file management. Built with Next.js, TypeScript, Tailwind CSS, and Shadcn-ui, utilizing Convex for the real-time database, Clerk for authentication, and EdgeStore for file storage.

Tokens
3.7K
Snippets
16
Records
18
Agent score
34%

What's inside Zotion

  1. Overview of Zotion features and technologies

    master

    Zotion is a simplified Notion clone built with a modern full-stack architecture.

    Core Features

    • Editor: Notion-style editor using Blocknote.
    • Organization: Infinite hierarchical document nesting, drag-and-drop reordering (via dnd-kit), and document pinning.
    • Navigation: Expandable/collapsible sidebar and real-time icon customization.
    • Data & Files: Real-time updates via Convex and file management (upload/delete/replace) via EdgeStore.
    • User Experience: Light/Dark mode, mobile responsiveness, and document cover images.
    • Security: Authentication via Clerk and the ability to publish notes to the web.

    Technology Stack

    • Framework: Next.js, TypeScript, Tailwind CSS, Shadcn-ui
    • Backend/Database: Convex (Real-time database)
    • Authentication: Clerk
    • File Storage: EdgeStore
    • Editor: Blocknote
    • Drag & Drop: dnd-kit
  2. Install and set up Zotion

    master

    To run Zotion locally, follow these steps to clone the repository, install dependencies, configure environment variables, and start the development servers.

    1. Clone and Install

    Clone the repository and install the necessary npm packages:

    npm install

    2. Configure Environment Variables

    Create a .env.local file by copying .env.example. You must populate this file with credentials from your Convex, Clerk, and EdgeStore dashboards.

    3. Start Services

    First, start the Convex backend development environment:

    npx convex dev

    Then, in a separate terminal, start the Next.js development server:

    npm run dev
    npm install
    npx convex dev
    npm run dev
  3. Write and use Convex query functions

    master

    A query function in Convex is used to read data from the database. You define it using the query function, specifying argument validators with v and providing an async handler. Inside the handler, you access the database via ctx.db.

    To use a query function in a React component, import the useQuery hook and pass the API function reference along with the required arguments.

    // 1. Define the query function
    import { query } from "./_generated/server";
    import { v } from "convex/values";
    
    export const myQueryFunction = query({
      args: {
        first: v.number(),
        second: v.string(),
      },
      handler: async (ctx, args) => {
        const documents = await ctx.db.query("tablename").collect();
        return documents;
      },
    });
    
    // 2. Use the query in a React component
    const data = useQuery(api.functions.myQueryFunction, {
      first: 10,
      second: "hello",
    });
  4. Write and use Convex mutation functions

    master

    A mutation function in Convex is used to modify data (insert, update, or delete) in the database. You define it using the mutation function. Mutations can also read from the database using ctx.db just like queries.

    In React, you use the useMutation hook. This returns a function that you can call to trigger the mutation. You can either use it as a 'fire and forget' call or await the returned Promise to access the result of the mutation.

    // 1. Define the mutation function
    import { mutation } from "./_generated/server";
    import { v } from "convex/values";
    
    export const myMutationFunction = mutation({
      args: {
        first: v.string(),
        second: v.string(),
      },
      handler: async (ctx, args) => {
        const message = { body: args.first, author: args.second };
        const id = await ctx.db.insert("messages", message);
        return await ctx.db.get(id);
      },
    });
    
    // 2. Use the mutation in a React component
    const mutation = useMutation(api.functions.myMutationFunction);
    
    function handleButtonPress() {
      // Fire and forget
      mutation({ first: "Hello!", second: "me" });
      
      // OR use the result
      mutation({ first: "Hello!", second: "me" }).then((result) =>
        console.log(result)
      );
    }
  5. Configure environment variables for Zotion

    master

    Zotion requires specific environment variables from three different services to function correctly. Copy .env.example to .env.local and fill in the following keys:

    From Convex Dashboard:

    • CONVEX_DEPLOYMENT
    • NEXT_PUBLIC_CONVEX_URL
    • CLERK_JWT_ISSUER_DOMAIN

    From Clerk Dashboard:

    • NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
    • CLERK_SECRET_KEY

    From EdgeStore Dashboard:

    • EDGE_STORE_ACCESS_KEY
    • EDGE_STORE_SECRET_KEY
  6. Configure Clerk authentication in Convex

    master

    To enable Clerk authentication within your Convex backend, you must configure the convex/auth.config.js file. This file tells Convex which JWT issuer is authorized to authenticate users. You need to provide the Clerk issuer domain via the CLERK_JWT_ISSUER_DOMAIN environment variable and set the applicationID to "convex".

    export default {
      providers: [
        {
          domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
          applicationID: "convex",
        },
      ],
    };
  7. Define internal queries with internalQuery()

    master

    Use internalQuery to define functions that read from your Convex database but are not accessible from the client. They can only be called by other Convex functions (queries, mutations, or actions). The function receives a QueryCtx as its first argument.

    import { internalQuery } from "./_generated/server";
    
    export const myInternalQuery = internalQuery({ query: async (ctx) => {
      // ctx is QueryCtx
      return await ctx.db.query("tableName").collect();
    }});
  8. Define internal actions with internalAction()

    master

    Use internalAction to define actions that are not accessible from the client. They can only be called by other Convex functions. The function receives an ActionCtx as its first argument.

    import { internalAction } from "./_generated/server";
    
    export const myInternalAction = internalAction({ action: async (ctx, args) => {
      // ctx is ActionCtx
    }});
  9. Define HTTP actions with httpAction()

    master

    Use httpAction to define functions that respond to incoming HTTP requests. These must be routed in your convex/http.js file. The function receives an ActionCtx as its first argument and a Fetch API Request object as its second argument.

    import { httpAction } from "./_generated/server";
    
    export const myHttpAction = httpAction(async (ctx, request) => {
      // ctx is ActionCtx
      // request is a Fetch API Request object
      return new Response("Hello from Convex!");
    });
  10. Use EdgeStoreProvider and useEdgeStore for file management

    master

    The project exports EdgeStoreProvider and useEdgeStore (generated via createEdgeStoreProvider) to integrate EdgeStore into the React component tree. EdgeStoreProvider acts as the context provider, while useEdgeStore is a hook used to interact with the EdgeStore API within client components.

    import { EdgeStoreProvider, useEdgeStore } from '@/lib/edgestore';
    
    // Wrap your application or component tree
    <EdgeStoreProvider>
      <YourComponent />
    </EdgeStoreProvider>
    
    // Inside a child component
    const { uploadFile } = useEdgeStore();
  11. Define internal mutations with internalMutation()

    master

    Use internalMutation to define functions that modify your Convex database but are not accessible from the client. They can only be called by other Convex functions. The function receives a MutationCtx as its first argument.

    import { internalMutation } from "./_generated/server";
    
    export const myInternalMutation = internalMutation({ mutation: async (ctx, args) => {
      // ctx is MutationCtx
      await ctx.db.patch("id", { field: args.value });
    }});