DEV Community

Cover image for Build an Instagram clone with strapi.js and svelte (PART 2)
arnu515
arnu515

Posted on

Build an Instagram clone with strapi.js and svelte (PART 2)

Hey! I'm back here with part 2 of this tutorial. As promised, now, we'll be working on the frontend.

As always, the code is available on Github

Interacting with the Strapi API

I was supposed to put this in the previous part, but I forgot 🤦‍♂️

We're currently using Strapi on localhost:1337. This is fine for now, but in production, this URL will change. If we use this URL on every request we make, we may have to change this to the production url a lot of times. Instead of having to change it so much, let's only change it once. In main.ts, let's pass a prop called strapiApiUrl to our svelte app.

// src/main.ts
import App from './App.svelte';

const app = new App({
    target: document.body,
    props: {
        strapiApiUrl: 'http://localhost:1337'
    }
});

export default app;
Enter fullscreen mode Exit fullscreen mode

Now, in App.svelte, we can set this strapiApiUrl as a context so that we can access it from anywhere within our app. We can achieve this with getContext and setContext.

<!-- src/App.svelte -->

<script lang="ts">
    // ...
    import { setContext } from "svelte";

    // ...

    export let strapiApiUrl: string;
    setContext("apiUrl", strapiApiUrl);
</script>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

Making API requests

We can use fetch, but I think that axios is better, so let's install it:

npm install axios
Enter fullscreen mode Exit fullscreen mode

Now, to make an API request,

import axios from "axios";
import { getContext } from "svelte";

async function makeApiRequest(): Promise<WhatTheApiReturns> {
    try {
        const { data } = await axios.get<WhatTheApiReturns>(
            getContext("apiUrl") + "/path/of/the/api"
        );
        return data;
    } catch (err) {
        console.log({ error: err });
        throw new Error(
            "Request failed with status: " +
                err.response.status +
                "\nCheck the console for further details."
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

You can replace get with post or delete for those methods.

The post component

Let's quickly scaffold up a Post component which will show our post, like on instagram:

Alt Text

Let's see the code for this Post component:

<!-- src/components/Post.svelte -->

<script lang="ts">
    import { getContext } from "svelte";

    // typescript only
    import type { Post } from "../types";

    export let post: Post;
</script>

<style>
    .post {
        width: 50%;
        margin: 0 auto;
    }

    @media (max-width: 992px) {
        .post {
            width: 70%;
        }
    }

    @media (max-width: 600px) {
        .post {
            width: 90%;
        }
    }
</style>

<div class="w3-card post w3-section">
    <a href="/@{post.user.username}/{post.id}"><img
            src={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}
            alt={post.image.alternativeText || 'Post image'}
            style="width: 100%" /></a>
    <div class="w3-container">
        <p class="w3-small w3-text-gray">
            <a
                href="/@{post.user.username}"
                style="text-decoration: none">@{post.user.username}</a>
        </p>
        <p>{post.content}</p>
    </div>
    <footer class="w3-bar w3-border-top w3-border-light-gray">
        <a
            href="/@{post.user.username}/{post.id}"
            class="w3-bar-item w3-button w3-text-blue"
            style="width:100%">{post.comments.length}
            {post.comments.length === 1 ? 'Comment' : 'Comments'}</a>
    </footer>
</div>
Enter fullscreen mode Exit fullscreen mode

Now, if you want the types.ts file which contains the types like Post and Comment, here it is:

// WARNING: TYPESCRIPT USERS ONLY!

export interface User {
    id: string;
    username: string;
    email: string;
    provider: string;
    confirmed: boolean;
    blocked: any;
    role: number;
    created_at: string;
    updated_at: string;
}

export interface Post {
    id: number;
    user: User;
    content: string;
    image: Image;
    comments: Comment[];
    published_at: string;
    created_at: string;
    updated_at: string;
}

export interface Comment {
    id: number;
    content: number;
    user: null | User;
    post: number;
    published_at: string;
    created_at: string;
    updated_at: string;
}

interface Image {
    id: number;
    name: string;
    alternativeText: string;
    caption: string;
    width: number;
    height: number;
    formats: {
        thumbnail: ImageMetaData;
        large: ImageMetaData;
        medium: ImageMetaData;
        small: ImageMetaData;
    };
    hash: string;
    ext: string;
    mime: string;
    size: number;
    url: string;
    previewUrl: null | string;
    provider: string;
    provider_metadata: null | any;
    created_at: string;
    updated_at: string;
}

interface ImageMetaData {
    name: string;
    hash: string;
    ext: string;
    mime: string;
    width: number;
    height: number;
    size: number;
    path: null | string;
    url: number;
}
Enter fullscreen mode Exit fullscreen mode

Fetching posts from the API

Let's create a /posts route. That route will serve the posts component, which fetches posts from our Strapi CMS and renders them.

<!-- src/routes/posts.svelte -->

<script lang="ts">
    import axios from "axios";
    import { getContext } from "svelte";
    import Post from "../components/Post.svelte";
    import type { Post as PostType } from "../types";

    async function getPosts(): Promise<PostType[]> {
        try {
            const { data } = await axios.get<PostType[]>(
                getContext("apiUrl") + "/posts"
            );
            return data;
        } catch (err) {
            console.log({ error: err });
            throw new Error(
                "Request failed with status: " +
                    err.response.status +
                    "\nCheck the console for further details."
            );
        }
    }
</script>

<h1 class="w3-xxxlarge w3-center">Posts</h1>
{#await getPosts()}
    <div class="w3-center w3-section w3-xxlarge w3-spin">
        <i class="fas fa-spinner" />
    </div>
{:then posts}
    <div class="w3-container w3-margin">
        {#each posts as post}
            <Post {post} />
        {/each}
    </div>
{:catch err}
    <div
        class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
        {err}
    </div>
{/await}
Enter fullscreen mode Exit fullscreen mode

We need to add this route to our router, so let's quickly do that by adding this line to App.svelte:

router("/posts", setupRouteParams, () => (page = Posts));
Enter fullscreen mode Exit fullscreen mode

Now, create a post on strapi following the video below.

If we head over to /posts on our frontend, we see:

Alt Text

Individual post page

When we visit @username/postid, I want to see username's post with ID postid, so let's do just that

<!-- src/routes/onePost.svelte -->

<script lang="ts">
    import axios from "axios";
    import { getContext } from "svelte";
    import router from "page";

    import type { Post, Comment as CommentType } from "../types";
    import Comment from "../components/Comment.svelte";

    export let params: { username: string; postId: string };
    const apiUrl = getContext("apiUrl");

    async function getPost(): Promise<Post> {
        try {
            const { data } = await axios.get<Post>(
                apiUrl + "/posts/" + params.postId
            );
            if (data.user)
                if (data.user.username !== params.username)
                    router.redirect("/404");
            return data;
        } catch (err) {
            if (err.response.status === 404) router.redirect("/404");
            else {
                console.log({ error: err });
                throw new Error(
                    "Request failed with status: " +
                        err.response.status +
                        "\nCheck the console for further details."
                );
            }
        }
    }

    async function getComments(post: Post): Promise<CommentType[]> {
        try {
            let comments: CommentType[] = [];
            for (let i = 0; i < post.comments.length; i++) {
                const { data } = await axios.get<CommentType>(
                    apiUrl + "/comments/" + post.comments[i].id
                );
                comments.push(data);
            }

            return comments;
        } catch (err) {
            if (err.response) {
                console.log({ err });
                if (err.response.status === 404) router.redirect("/404");
                else {
                    console.log({ error: err });
                    throw new Error(
                        "Request failed with status: " +
                            err.response.status +
                            "\nCheck the console for further details."
                    );
                }
            } else throw new Error(err);
        }
    }
</script>

<style>
    .post {
        width: 50%;
        margin: 0 auto;
    }

    @media (max-width: 992px) {
        .post {
            width: 70%;
        }
    }

    @media (max-width: 600px) {
        .post {
            width: 90%;
        }
    }
</style>

{#await getPost()}
    <div class="w3-center w3-section w3-xxlarge w3-spin">
        <i class="fas fa-spinner" />
    </div>
{:then post}
    <div class="w3-card post">
        <a
            href={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}><img
                src={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}
                alt={post.image.alternativeText || 'Post image'}
                style="width: 100%" /></a>
        <div class="w3-container">
            <p class="w3-small w3-text-gray">
                <a
                    href="/@{post.user.username}"
                    style="text-decoration: none">@{post.user.username}</a>
            </p>
            <p>{post.content}</p>
        </div>
    </div>

    <div class="w3-card post w3-margin-top">
        <header class="w3-container w3-border-bottom w3-border-light-gray">
            <h3>Comments</h3>
        </header>
        <div class="w3-container">
            {#await getComments(post)}
                <div class="w3-center w3-section w3-xxlarge w3-spin">
                    <i class="fas fa-spinner" />
                </div>
            {:then comments}
                {#each comments as comment}
                    <Comment {comment} />
                {/each}
            {:catch err}
                <div
                    class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
                    {err}
                </div>
            {/await}
        </div>
    </div>
{:catch err}
    <div
        class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
        {err}
    </div>
{/await}
Enter fullscreen mode Exit fullscreen mode

We have to get each comment separately because Strapi doesn't return detailed information of a relation inside another relation. This can be good if you don't need this data, which in most cases, you don't, but if you're in situations like this, you'll have to use this method.

Now this is what our app should look like:

Alt Text

User profile

Let's work on the user's profile page. This will be accessed through the /@username route. We want to display every post this user has made. So, create src/routes/userProfile.svelte, and put this in it:

<!-- src/routes/userProfile.svelte -->

<script lang="ts">
    import axios from "axios";
    import PostComponent from "../components/Post.svelte"
import { getContext } from "svelte";
import type { Post } from "../types";

    export let params: {username: string}
    const apiUrl:string = getContext("apiUrl")

    async function getPosts(): Promise<Post[]> {
        try {
            const { data } = await axios.get<Post[]>(
                getContext("apiUrl") + "/posts"
            );
            return data.map(post => {
                if (post.user.username === params.username) return post
            });
        } catch (err) {
            console.log({ error: err });
            throw new Error(
                "Request failed with status: " +
                    err.response.status +
                    "\nCheck the console for further details."
            );
        }
    }
</script>

<h1 class="w3-xxxlarge w3-center">Posts</h1>
{#await getPosts()}
    <div class="w3-center w3-section w3-xxlarge w3-spin">
        <i class="fas fa-spinner" />
    </div>
{:then posts}
    <div class="w3-container w3-margin">
        {#each posts as post}
            <PostComponent {post} />
        {/each}
    </div>
{:catch err}
    <div
        class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
        {err}
    </div>
{/await}
Enter fullscreen mode Exit fullscreen mode

And here's the user's page:

Alt Text

Conclusion

And here we are! We've seen how easy it is to integrate with Strapi's API using your frontend, but, we've only dealt with unauthenticated requests. In the next part, you'll see how to add authentication to your requests, and also create posts, comments and upload images to Strapi.

The 3rd part is out! Click here, here or here to view it!

Top comments (11)

Collapse
 
arnu515 profile image
arnu515

For those people getting an error stating cannot read property provider of undefined, you have to put post.image instead of post.image[0]. I accidently set the type of image on post to multiple in strapi. If you still have questions, check on the Github repository: github.com/arnu515/quickstagram

Collapse
 
itsmefarhan profile image
Farhan Farooq

How to handle following / followers in strapi?

Collapse
 
arnu515 profile image
arnu515

You could create a followers table in the database, which would contain the user it belongs to, and users they follow as relationships. This will allow you to get the Followers and Following numbers you see on instagram.

For the frontend, you could setup a custom filter which would show, for example, only the posts of the people the user followed.

Collapse
 
siemensubbaiah profile image
siemen_subbaiah

Hey there, I know it is a super late reply!
but can you elaborate here, I'm primarily a frontend dev!

Thread Thread
 
arnu515 profile image
arnu515

No worries! Strapi makes it really easy for you.

You need to create a table called "followers" which will basically act as a link between two users - a user who is following another user. This kind of relationship is called a Many-to-Many relationship, which means that many users can follow many other users. While creating a relationship, you can select the many-to-many option and choose the tables in strapi itself.

Collapse
 
rifie profile image
Syarifah Riefandania ☄

Hi, nice tutorial... I got "Uncaught ReferenceError: qs is not defined" on main.ts. I peeked your github, the "qs" is not there either.. please help? thanks!

Collapse
 
linehammer profile image
linehammer

If you are using any script file and getting "Uncaught ReferenceError:" which means 'x' is either a variable or a method which you are trying to use before declaring it using var keyword. This means that there is a non-existent variable referenced somewhere. This variable needs to be declared, or you need to make sure it is available in your current script or scope otherwise , it will endup throwing this 'x' is not defined error . This usually indicates that your library is not loaded and JavaScript does not recognize the 'x'.

To solve this error: Load your library at the beginning of all your scripts.

There can be multiple other reasons for this issue:

Path to CDN library you included is not correct
The library file is corrupted
Working offline
Conflict with Other Libraries

Collapse
 
jimdonaghy profile image
Jim Donaghy

You'll need to install the qs package: "npm install qs"

Collapse
 
shriji profile image
Shriji

Excellent work 👍

Collapse
 
arnu515 profile image
arnu515

Thank you so much!

Collapse
 
arnu515 profile image
arnu515

The third part is out! dev.to/arnu515/build-an-instagram-...