DEV Community

Cover image for #30DaysOfAppwrite : Create Blog Posts
Christy Jacob for Appwrite

Posted on • Edited on

#30DaysOfAppwrite : Create Blog Posts

Intro

#30DaysOfAppwrite is a month-long event focused on giving developers a walk-through of all of Appwrite's features, starting from the basics to more advanced features like Cloud Functions! Alongside, we will also be building a fully-featured Medium clone to demonstrate how these
concepts can be applied when building a real-world app. We also have some exciting prizes for developers who follow along with us!

Retrieving Blog Posts

Welcome back to another session on the Appwrite Database 👋 . We hope you have gone through the Day 17 article. It is important as we build upon the knowledge gained in Day 16 and 17. Now it's time to integrate our main feature into our app, Blog Posts.

We will use the Post Collection to have users create posts that are embedded into their profiles.

For this, we need to add several methods to our appwrite.js file:

  • Fetch all Posts
  • Fetch all Posts from a User
  • Fetch a single Post
  • Create/Edit/Delete a Post

The first method to add will be the one that fetches all posts. Technically, we want to retrieve the latest 25 posts with the listDocuments method, sorted from new to old, which have the published attribute to true from the server. For this, we are going to add the following to the appwrite.js file:

export const api = {
    //...
    fetchPosts: (limit, offset) => {
        return sdk.database.listDocuments(
            postsCollection,
            [Query.equal("published", 1)],
            limit,
            offset,
            "created_at",
            "DESC",
            "int"
        );
    },
    //...
}
Enter fullscreen mode Exit fullscreen mode

To fetch all posts from a user, we are going to write a similar method - except that we will filter by a User ID in the user_id attribute:

export const api = {
  //...
    fetchUserPosts: userId => {
        return sdk.database.listDocuments(
            postsCollection,
            [
                Query.equal("published", 1),
                Query.equal("user_id", userId),
            ],
            100,
            0,
            "created_at",
            "DESC",
            "int"
        );
  },
  //...
}
Enter fullscreen mode Exit fullscreen mode

To fetch a single post, we will use the getDocument method where we can pass an ID, instead of the previously used listDocuments.

export const api = {
    //...
    fetchPost: id => sdk.database.getDocument(postsCollection, id),
    //...
}
Enter fullscreen mode Exit fullscreen mode

And for deleting a post, we can use the deleteDocument method like this:

export const api = {
    //...
    deletePost: id => sdk.database.deleteDocument(postsCollection, id),
    //...
}
Enter fullscreen mode Exit fullscreen mode

We will also add two more methods to create and edit a post, these will use createDocument and updateDocument respectively.

export const api = {
    createPost: async (data, userId, profileId) => {
        return sdk.database.createDocument(
            postsCollection,
            "unique()",
            data,
            ["role:all"],
            [`user:${userId}`]
        );
    },
    updatePost: async (id, data, userId) => {
        return sdk.database.updateDocument(
            postsCollection,
            id,
            data,
            ["role:all"],
            [`user:${userId}`]
        );
    },
}
Enter fullscreen mode Exit fullscreen mode

We will also quickly create the src/lib/Author.svelte file and add the following contents to it:

<script>
    import { api } from "../appwrite";
    import Avatar from "./Avatar.svelte";
    import Loading from "./Loading.svelte";
    export let user;
    const getAvatar = name => api.getAvatar(name);
    const fetchUser = api.fetchUser(user);
</script>

<a class="author" href={`#/profile/${user}`}>
    {#await fetchUser}
        <Loading />
    {:then author}
        <Avatar src={getAvatar(author.name)} />
        <h3>{author.name}</h3>
    {/await}
</a>

<style>
    a.author {
        display: flex;
        align-items: center;
        gap: 1rem;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

We will also create the src/lib/Avatar.svelte file and write this:

<script>
    export let src;
</script>

<img {src} alt="" />

<style>
    img {
        border-radius: 100%;
        height: 3rem;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Next we will introduce the API functionality for the newly created Author component, go ahead and open up appwrite.js and update the code to add the following new method:

export const api = {
    getAvatar: name => {
        return sdk.avatars.getInitials(name);
    },
}
Enter fullscreen mode Exit fullscreen mode

Now that we have all the API requests ready to retrieve blog posts, we now need to add Routes and Components for it. For this, we edit the src/routes/Index.svelte file, which will display all blog posts.

<script>
    import md from "snarkdown";
    import Loading from "../lib/Loading.svelte";
    import Action from "../lib/Action.svelte";
    import Author from "../lib/Author.svelte";
    import Preview from "../lib/Preview.svelte";
    import { api } from "../appwrite";
    const data = api
        .fetchPosts(25, 0)
        .then(r => r.documents)
        .then(posts => {
            return {
                promoted: posts[0],
                featured: posts.slice(1, 5),
                latest: posts.slice(5),
            };
        });
</script>

{#await data}
    <Loading />
{:then { promoted, featured, latest }}
    <section class="top">
        <div class="promoted">
            {#if promoted.cover}
                <img src={promoted.cover} alt={promoted.title} />
            {/if}
            <h2>{promoted.title}</h2>
            <Author user={promoted.user_id} />
            <p>
                {@html md(promoted.text)}
            </p>
            <Action href={`#/post/${promoted.$id}`}>Read more</Action>
        </div>
        <div class="cards">
            {#each featured as feature}
                <a class="card" href={`#/post/${feature.$id}`}>
                    {#if feature.cover}
                        <img
                            src={feature.cover}
                            alt={feature.title} />
                    {/if}
                    <h2>{feature.title}</h2>
                </a>
            {/each}
        </div>
    </section>
    <h1>Latest</h1>
    <section class="latest">
        {#each latest as post}
            <Preview {post} />
        {/each}
    </section>
{/await}

<style>
    section.top {
        display: flex;
        justify-content: space-evenly;
        gap: 1rem;
    }
    section.latest {
        display: flex;
        flex-wrap: wrap;
        flex-direction: row;
        justify-content: center;
        align-items: auto;
        align-content: start;
        gap: 1rem;
    }
    img {
        width: 100%;
    }
    .promoted img {
        border-radius: 0.5rem;
    }
    .cards {
        display: flex;
        flex-direction: column;
        gap: 3rem;
    }
    .cards .card {
        font-size: 0.75rem;
        display: flex;
        border-radius: 0.5rem;
        align-items: center;
        gap: 0.5rem;
        background-color: white;
        transition: all 0.2s;
    }
    .cards .card:hover {
        background-color: #f02e65;
        color: white;
        transform: scale(1.05);
    }
    .card img {
        width: 50%;
        height: 100%;
        border-radius: 0.5rem;
        object-fit: cover;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

In this example, the fetchPosts() method retrieves the latest 25 posts from our Database and splits them up into the following object structure:

  • Promoted - The latest Post
  • Featured - The next 4 posts subsequent to Promoted
  • Latest - All the remaining Posts

On Day 17, we created a Profile page, but there were no posts yet. To add this feature, we will revisit src/routes/Profile.svelte and update the following code.

<script>
    import Preview from "../lib/Preview.svelte";
    import MyPost from "../lib/MyPost.svelte";
    //...
    const fetchUser = () => api.fetchUser(params.id);
    const fetchPosts = () => api.fetchUserPosts(params.id).then(r => r.documents);
    let all = Promise.all([fetchUser(), fetchPosts()]);
</script>

<section>
    {#await all}
        <Loading />
    {:then [author, posts]}
        <section class="author">
            <h3>{author.name}</h3>
        </section>
        {#if $state.user.$id == params.id}
            <h1>My Posts</h1>
            <p><a class="button" href="/create" use:link>Create</a></p>
            <section class="my-post">
                {#each posts as post}
                    <MyPost on:deleted={() => {all = Promise.all([fetchUser(), fetchPosts()]); console.log("deleted")} } {post} />
                {/each}
            </section>
        {:else}
            <h1>Latest Posts</h1>
            <section class="latest">
                {#each posts as post}
                    <Preview {post} />
                {/each}
            </section>
        {/if}
    {:catch error}
        {error}
        <p>
            Public profile not found
            <a href="/profile/create" use:link>Create Public Profile</a>
        </p>
    {/await}
</section>

Enter fullscreen mode Exit fullscreen mode

We are using two components here that haven't been created yet. MyPost is an editable component that will be shown only to the post owner and allow them to edit and delete their posts.

On the other hand, the Preview component is a read-only component that is used solely to display a preview of a blog post. We will reuse this component in the Index route.

src/lib/Preview.svelte

<script>
    export let post;
</script>

<a href={`#/post/${post.$id}`}>
    {#if post.cover}
        <img
            class="cover"
            src={post.cover}
            alt={post.title} />
    {/if}
    <h2>{post.title}</h2>
</a>

<style>
    img.cover {
        width: 100%;
        border-radius: 0.5rem;
    }
    a {
        display: flex;
        flex-direction: column;
        justify-content: flex-start;
        align-items: center;
        border-radius: 0.5rem;
        background-color: white;
        max-width: 18rem;
        font-size: 1.1rem;
        line-height: 2rem;
        transition: all 0.2s;
    }
    a:hover {
        background-color: #f02e65;
        color: white;
        transform: scale(1.05);
    }
    h2 {
        font-size: 1.1rem;
        margin: 0.5rem;
        text-align: center;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

src/lib/MyPost.svelte

<script>
    import { createEventDispatcher } from "svelte";
    import { link } from "svelte-spa-router";
    import { api } from "../appwrite";
    export let post;
    const dispatch = createEventDispatcher()
    const deletePost = async id => {
        if (confirm("are you sure you want to delete?")) {
            await api.deletePost(id);
            dispatch('deleted');
        }
    };
</script>

<article class="card">
    {#if post.cover}
        <img
            class="cover"
            src={post.cover}
            alt={post.title} />
    {/if}
    <h2>{post.title}</h2>
    <a href="/post/{post.$id}" use:link class="button">Preview</a>
    <a href="/post/{post.$id}/edit" use:link class="button">Edit</a>
    <a
        href="/delete"
        on:click|preventDefault={() => deletePost(post.$id)}
        class="button">Delete</a>
</article>

<style>
    article.card {
        background-color: white;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        border-radius: 0.5rem;
    }
    img.cover {
        width: 8rem;
        border-top-left-radius: 0.5rem;
        border-bottom-left-radius: 0.5rem;
    }
    h2 {
        font-size: 1.1rem;
        margin: 0.5rem;
        text-align: center;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Now the component to display a single blog post is left. For this, we are going to create src/routes/Post.svelte with the following content:

<script>
    import md from "snarkdown";
    import Loading from "../lib/Loading.svelte";
    import Author from "../lib/Author.svelte";
    import { api } from "../appwrite";

    export let params = {};

    let postFetch = api.fetchPost(params.slug);
</script>

{#await postFetch}
    <Loading />
{:then post}
    <h1>
        {post.title}
    </h1>
    <Author user={post.user_id} />
    {#if post.cover}
        <img class="cover" src={post.cover} alt={post.title} />
    {/if}
    <section class="content">
        {@html md(post.text)}
    </section>
    <h2>Comments</h2>
{/await}

<style>
    img.cover {
        width: 100%;
        border-radius: 0.5rem;
    }
    section.content {
        font-size: 1.1rem;
        line-height: 2rem;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Now all blog posts can be read. Unfortunately, we have no way to verify this as our users cannot create posts yet. We will take care of this in the next section.

Creating Blog Posts

Now we are going to add the first component, which is going to write data to our Appwrite database. For this, we are going to add the src/routes/Create.svelte file and fill it with the following content:

<script>
    import EasyMDE from "easymde";
    import { api } from "../appwrite";
    import { state } from "../store";
    import { onMount } from "svelte";
    import { replace } from 'svelte-spa-router';
    import "../../node_modules/easymde/dist/easymde.min.css";
    import Loading from "../lib/Loading.svelte";
    export let params = {};
    let published = false,
        title = "",
        easyMDE,
        message = "",
        loading = false,
        cover,
        post,
        content = "";
    let postFetch = async () => {
        post = await api.fetchPost(params.slug);
        title = post.title;
        easyMDE.value(post.text);
        cover = post.cover;
    };
    onMount(() => {
        if (params.slug) {
            postFetch();
        }
        easyMDE = new EasyMDE({ element: document.getElementById("content"), renderingConfig: {
            singleLineBreaks: true,
        } });
    });
    const submit = async () => {
        message = "";
        loading = true;
        let content = easyMDE.value();
        if (title.trim() == "" || content.trim() == "") {
            message = "Title and content are both required";
            console.log("title and content are both required");
            loading = false;
            return;
        }
        console.log({
            title: title,
            text: content,
            published: published,
            user: $state.user.$id,
            profile: $state.profile.$id,
        });
        try {
            let data = {
                    title: title,
                    text: content,
                    published: published,
                    user_id: $state.user.$id,
                    created_at: params.slug ? post.created_at :  new Date().getTime(),
                };
            if(params.slug) {
                //update
                await api.updatePost(params.slug,data,$state.user.$id)
                replace('/profile/'+$state.user.$id);
            } else {
                await api.createPost(
                    data,
                    $state.user.$id,
                    $state.profile.$id
                );
                easyMDE.value("");
                title = "";
                content = "";
                console.log("post created successfully");
                message = "Post created successfully";
            }
        } catch (error) {
            console.log(error);
            message = error;
        } finally {
            loading = false;
        }
    };
</script>

<section>
    {#if params.slug}
        <h2>Edit Post</h2>
    {:else}
        <h2>Create Post</h2>
    {/if}
    {#if message}
        <div class="alert">{message}</div>
    {/if}
    <form on:submit|preventDefault={submit}>
        <label for="title">Title</label>
        <input
            required
            type="text"
            placeholder="Enter title"
            bind:value={title} />
        <label for="content">Content</label>
        <textarea
            bind:value={content}
            name="content"
            id="content"
            cols="30"
            rows="10"
            placeholder="Enter content" />
        <label for="status">Status</label>
        <select name="status" id="status" bind:value={published}>
            <option value={false}>Draft</option>
            <option value={true}>Published</option>
        </select>
        <button disabled={loading ? true : false} class="button" type="submit"
            >{ params.slug ? 'Save' : 'Create'}</button>
    </form>
</section>

<style>
    form {
        display: flex;
        flex-direction: column;
    }
    label {
        margin-top: 1rem;
    }
    .alert {
        background-color: #ff000066;
        padding: 1rem;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

This allows users to create and edit their posts. The final step is to add all the components to our router over at src/App.svelte.

<script>
    //...
     import Post from "./routes/Post.svelte";
     import Create from "./routes/Create.svelte";
    //..    
    const routes = {
        //...
        "/create": Create,
        "/post/:slug": Post,
        "/post/:slug/edit": Create
    };
</script>
Enter fullscreen mode Exit fullscreen mode

Credits

We hope you liked this write-up. You can follow #30DaysOfAppwrite on Social Media to keep up with all of our posts. The complete event timeline can be found here

Feel free to reach out to us on Discord if you would like to learn more about Appwrite, Aliens or Unicorns 🦄. Stay tuned for tomorrow's article! Until then 👋

Top comments (1)

Collapse
 
visvajeet profile image
Visva Jeet

How to fetch all posts with user profile?