DEV Community

loading...
Cover image for #30DaysOfAppwrite : Create Blog Posts
Appwrite

#30DaysOfAppwrite : Create Blog Posts

Christy Jacob
・7 min read

Intro

#30DaysOfAppwrite is a month long event focused at 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 is going to 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 following to the appwrite.js file:

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

To fetch all Posts from a user, our method is going to look similar - except that we will filter by a User ID in the user_id attribute:

export const api = {
  //...
    fetchUserPosts: userId => {
        return sdk.database.listDocuments(
            postsCollection,
            [
                `user_id=${userId}`,
                "published=1"
            ],
            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

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 is retrieving that 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 owner of a post and allow them to edit and delete their posts.

The Preview component on the other hand is a read-only component that will solely be used 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 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 lack the ability to create posts. 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 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 user to create and edit their posts. Now 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 👋

Discussion (0)