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"
);
},
//...
}
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"
);
},
//...
}
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),
//...
}
And for deleting a post, we can use the deleteDocument
method like this:
export const api = {
//...
deletePost: id => sdk.database.deleteDocument(postsCollection, id),
//...
}
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}`]
);
},
}
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>
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>
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);
},
}
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>
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>
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>
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>
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>
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>
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>
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)
How to fetch all posts with user profile?