Welcome to the second post in our Almost Netflix series! We'll be building upon the project setup from yesterday and build a Web frontend for our Netflix Clone! In this post, we will take a closer look at building the clone using VueJS. In the subsequent posts of this series, we'll be building frontends for other platforms like Flutter, iOS and Android!
This one's all about the Web, so let's get started!
It would be impossible to write every piece of code in this article π¬ You will read about all essential concepts, components, and communication with Appwrite. Still, if you want to check out every corner of our Almost Netflix web application, you can check out the GitHub Source Code that holds the whole application.
I decided to host the project on Vercel! You can check out the preview of Netflix Clone live demo.
π€ What is Appwrite?
Appwrite is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, databases, file storage, cloud functions, webhooks, and much more! If anything is missing, you can extend Appwrite using your favorite backend language.
π Requirements
Before we begin, we should have the Appwrite instance up and running, with the Almost Netflix project set up. If you haven't setup the project yet, you can refer to our previous blog post.
To build Almost Netflix, we will use Vue.js because of its decent simplicity and forced structure. I believe reading Vue components is straightforward, and any web developer can understand what the code is trying to achieve.
To manage routing, importing, and folder structure, we will stick to NuxtJS, an intuitive Vue framework.
Last but not least, we will use Tailwind CSS to style the components. Tailwind CSS makes it a bit harder to read HTML code but allows speedy prototyping, allowing us to recreate the Netflix UI in a blink of an eye.
No more, I promise! If you don't know some technologies used in this project, this might be the best moment to continue the article to start learning them. All in all, we are developers, and we need to learn every day π Fun fact, I learned NuxtJS with this project.
π οΈ Create Nuxt project
Thanks to fantastic Tailwind CSS documentation, we can visit their Install Tailwind CSS with Nuxt.js docs that will take us step-by-step creating the NuxtJS project and adding Tailwind CSS.
Once we have the project set up, we remove all files from the components
and pages
folders. These contain templates to get us started, but we won't need that π To see our setup working, let's create file pages/index.vue
and put simple HTML in it:
<template>
<h1 class="text-blue-500 text-4xl">
Almost Netflix π¬
</h1>
</template>
Make sure the npm run dev
is still running in the background. We can visit http://localhost:3000/
and see our big blue title if everything works well.
Let's customize our project a little by using custom fonts. I decided to use Inter font as it's pretty close to Netflix one. Thanks to Google Fonts, we can do tiny changes to our assets/css/main.css
to update all fonts on our website:
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
font-family: 'Inter', sans-serif;
}
Lastly, let's prepare all assets in our project by copying them from static folder on GitHub. All we need to do is download them and place them into our static
folder. This will ensure we have all logos, icons, and backgrounds ready to be used in HTML later.
Great, the project is ready! Let's continue by preparing Appwrite services to communicate with the Appwrite server.
π€ Appwrite Service
We create the file services/appwrite.ts
and prepare a few functions to get the hang of it. We will use this file for direct communication with Appwrite SDK. By doing this, we separate server-communication logic from the rest of the application logic, resulting in more readable code.
Let's start by preparing the Appwrite SDK variable:
import { Appwrite, Models, Query } from "appwrite";
const sdk = new Appwrite();
sdk
.setEndpoint("http://localhost/v1")
.setProject("almostNetflix");
Make sure to use your own endpoint and project ID. Please don't ask what happened to
almostNetfix1
. I am not proud of that π
Since we are using TypeScript, let's also add definitions, so we can use them later to describe what data we are getting from Appwrite:
export type AppwriteMovie = {
name: string,
description: string,
durationMinutes: number,
thumbnailImageId: string,
releaseDate: number,
ageRestriction: string,
relationId?: string
} & Models.Document;
export type AppwriteWatchlist = {
movieId: string,
userId: string
} & Models.Document;
Now that we have types and SDK ready let's create and export AppwriteService
itself. Inside, let's also add a few functions for authentication, so we have a starting point for upcoming authentication components:
export const AppwriteService = {
// Register new user into Appwrite
async register(name: string, email: string, password: string): Promise<void> {
await sdk.account.create("unique()", email, password, name);
},
// Login existing user into his account
async login(email: string, password: string): Promise<void> {
await sdk.account.createSession(email, password);
},
// Logout from server removing the session on backend
async logout(): Promise<boolean> {
try {
await sdk.account.deleteSession("current");
return true;
} catch (err) {
// If error occured, we should not redirect to login page
return false;
}
},
// Figure out if user is logged in or not
async getAuthStatus(): Promise<boolean> {
try {
await sdk.account.get();
return true;
} catch (err) {
// If there is error, user is not logged in
return false;
}
},
};
Perfect! Now we have our AppwriteService ready to be used by the Vue application and a few authentication functions already set up. We can revisit this file any time in the future and add more functions to make sure this file is our "gateway" to Appwrite.
With AppwriteService ready for authentication, we should implement Vue components for that, right?
π Authentication
Before we start, let's update our pages/index.vue
to have a welcome message and buttons to redirect a visitor to login and register pages. Since I don't want to make this article about HTML and Tailwind CSS, you can check out the Index file on GitHub.
We can copy pages/login.vue
from login file and pages/register.vue
from register file exactly the same way, although we will take a closer look at these two.
When copying index, login, and register files,
middleware
is already configured on them. You might need to temporarily remove those for pages to load correctly. We will be creating middlewares in upcoming sections.
In pages/login.vue
, we create a form and listen to its submission:
<form @submit.prevent="onLogin()">
<input v-model="email" type="email" />
<input v-model="pass" type="password"/>
<button type="submit">Sign In</button>
</form>
We then create the onLogin
method where we talk to AppwriteService and redirect to the application after successful login:
export default Vue.extend({
data: () => {
return {
email: '',
pass: '',
}
},
methods: {
async onLogin() {
try {
await AppwriteService.login(this.email, this.pass)
this.$router.push('/app')
} catch (err: any) {
alert(err.message)
}
},
},
})
You can also notice we use data for in-component state management, and thanks to the v-model
Vue attribute, the value from the input is automatically stored in the variable.
Looking at pages/register.vue
, we do the same process with different values. The only main difference is in our onRegister
function (alternative to onLogin
), which also validates if passwords match and if the user agrees with terms:
export default Vue.extend({
data: () => {
return {
name: '',
email: '',
pass: '',
passAgain: '',
agreeTerms: false,
}
},
methods: {
async onRegister() {
if (this.pass !== this.passAgain) {
alert('Passwords need to match.')
return
}
if (!this.agreeTerms) {
alert('You have to agree to our terms.')
return
}
try {
await AppwriteService.register(this.name, this.email, this.pass)
await AppwriteService.login(this.email, this.pass)
this.$router.push('/app')
} catch (err: any) {
alert(err.message)
}
},
},
})
Notice that right after we register, we also login the user with the same credentials. This allows us to redirect the user directly to the application instead of asking them to login.
To finish the login process, we need to create pages/app/index.vue
, the first page the user sees when they log in. Actually, let me show you a trick here...
When a user logs in, I want them to see a list of all movies, but I also want to URL to be app/movies
. This will allow me to make pages like app/watchlist
, app/profiles
or app/tv-shows
in the future.
We create a really simple pages/app/index.vue
component to achieve this. The only thing this component will do is redirect to the new path app/movies
:
<template></template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
middleware: [
function ({ redirect }) {
redirect('/app/movies')
},
],
})
</script>
Now we create a new file called pages/app/movies.vue
and put movies logic in there. In summary, after successful login, you will be redirected to /app
, but you won't even see this page because you will be redirected to /app/movies
straight away.
For now, let's put a simple greeting text into our pages/app/movies.vue
file:
<template>
<h1>Welcome logged in user π</h1>
</template>
We are done with authentication! Oh, wait... As I am playing with the website, I noticed I can manually change the URL in by browser to /app
and the application allows me to see movies page π¬ Let's look how we can use middleware to force redirects on specific pages depending on if user is logged in or not.
Authentication middlewares
Middleware can be used to restrict a user from visiting a specific pages. In our scenario, we don't want to allow the user to visit the movies page if they are not logged in. First, let's create middleware/only-authenticated.ts
with a simple logic that checks current user status and redirects to login if the user is not logged in:
import { Middleware } from "@nuxt/types";
import { AppwriteService } from "../services/appwrite";
const middleware: Middleware = async ({ redirect }) => {
const isLoggedIn = await AppwriteService.getAuthStatus();
if (isLoggedIn) {
// OK
} else {
return redirect("/login");
}
}
export default middleware;
Thanks to this middleware, the user will be allowed to visit the route if they are logged in but will be redirected if they are not. But what route? π€
To use this middleware, we need to apply it to a specific page. Since we don't want to allow a user to visit the movies page, we update pages/app/movies.ts
:
<template>
<h1>Welcome logged in user π</h1>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
middleware: 'only-authenticated',
})
</script>
Just like that β¨ we protected our page, and we only allow logged-in users to view our movies page. Real quick, let's do the exact opposite for the rest of the pages we currently have - let's redirect the user to the application if they are already logged in. We do this to prevent the user from getting to the login page if they are already logged in.
To achieve this, we create one more middleware in middleware/only-unauthenticated.ts
:
import { Middleware } from "@nuxt/types";
import { AppwriteService } from "../services/appwrite";
const middleware: Middleware = async ({ redirect }) => {
const isLoggedIn = await AppwriteService.getAuthStatus();
if (isLoggedIn) {
return redirect("/app");
} else {
// OK
}
}
export default middleware;
Notice, we did the exact opposite in this component. If a user is not logged-in, it's OK, but we forcefully redirect to the application page if they are.
Now, let's add this only-unauthenticated
middleware to all the 3 pages pages/index.vue
, pages/login.vue
and pages/register.vue
.
Let's try it! If we are logged in and try to visit /login
, we will jump back to the movies page. Great! We have successfully implemented middleware to protect our application's specific pages from unauthenticated users.
π Application layout
In every application, some parts repeat on all pages. In most cases, it's header and footer, but it could also be a hero section or live chat bubble. To prevent repeating this part of code, we can create a layout out of it and use layout on our pages, similar to how we used middleware. First, let's create a simple layout and use it on our movies page. To do that, we create layouts/app.vue
:
<template>
<h1>Header</h1>
<hr>
<Nuxt />
<hr>
<h1>Footer</h1>
</template>
We used a special HTML tag <Nuxt />
, which means, if a page is using this layout, the page's content will be placed exactly where we put our <Nuxt />
tag. This is really handy if we want to place a page in between the header and footer.
To use our app
layout, we mention it on our movie page. We simply update pages/app/movies.vue
:
<!-- ... -->
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
layout: 'app',
// ...
})
</script>
We can now see our header and footer wrap our movies page. Awesome! Let's create an actual Netflix layout, shall we?
First, let's update our AppwriteService since we will need to show the user's profile picture in the header. The header should also include a trending movie if we are on the landing page. To begin, let's create a function that gives us the user's profile picture:
export const AppwriteService = {
// ...
// Generate profile photo from initials
async getProfilePhoto(): Promise<URL> {
let name = "Anonymous";
try {
const account = await sdk.account.get();
if (account.name) {
// If we have name, use that for initials
name = account.name;
} else {
// If not, use email. That is 100% available always
name = account.email;
}
} catch (err) {
// Means we don't have account, fallback to anonymous image
}
// Generate URL from previously picked keyword (name)
return sdk.avatars.getInitials(name, 50, 50);
}
};
We should also prepare a function to preview the cover image of the movie. We will need a separate function for this because this main trending movie is covering the whole website with one huge image:
export const AppwriteService = {
// ...
// Same as above. Generates URL, setting some limits on size and format
getMainThumbnail(imageId: string): URL {
return sdk.storage.getFilePreview(imageId, 2000, undefined, "top", undefined, undefined, undefined, undefined, undefined, undefined, undefined, "webp");
}
};
Finally, let's implement a method to get featured movies from our database:
export const AppwriteService = {
// ...
// Simple query to get the most trading movie
async getMainMovie(): Promise<AppwriteMovie> {
const response = await sdk.database.listDocuments<AppwriteMovie>("movies", [], 1, undefined, undefined, undefined, ["trendingIndex"], ["DESC"]);
return response.documents[0];
}
};
With all of these methods ready, we can start using them in our layout. Let's visit app layout file on GitHub and copy its content to our page. Our layout looks lovely, and we have already got our first movie! This is starting to look almost like Netflix π
π¬ Movies page
We need to show rows of movies for different categories on our movies page, such as Popular this week
or New releases
. Before implementing this into our page, we will need methods to fetch data from Appwrite.
First of all, let's create categories configuration in one variable inside of our AppwriteService, that we can re-use later:
export type AppwriteCategory = {
title: string;
queries: string[];
orderAttributes: string[];
orderTypes: string[];
collectionName?: string;
}
export const AppwriteMovieCategories: AppwriteCategory[] = [
{
title: "Popular this week",
queries: [],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "Only on Almost Netflix",
queries: [
Query.equal("isOriginal", true)
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "New releases",
queries: [
Query.greaterEqual('releaseDate', 2018),
],
orderAttributes: ["releaseDate"],
orderTypes: ["DESC"]
},
{
title: "Movies longer than 2 hours",
queries: [
Query.greaterEqual('durationMinutes', 120)
],
orderAttributes: ["durationMinutes"],
orderTypes: ["DESC"]
},
{
title: "Love is in the air",
queries: [
Query.search('genres', "Romance")
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "Animated worlds",
queries: [
Query.search('genres', "Animation")
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "It's getting scarry",
queries: [
Query.search('genres', "Horror")
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "Sci-Fi awaits...",
queries: [
Query.search('genres', "Science Fiction")
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "Anime?",
queries: [
Query.search('tags', "anime")
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
{
title: "Thriller!",
queries: [
Query.search('genres', "Thriller")
],
orderAttributes: ["trendingIndex"],
orderTypes: ["DESC"]
},
];
export const AppwriteService = {
// ...
};
We just configured all the different categories we want to show on our homepage, each having a title, queries, and sorting configuration. Let's also prepare a function to get a list of movies where input is one of these categories:
export const AppwriteService = {
// ...
// List movies. Most important function
async getMovies(perPage: number, category: AppwriteCategory, cursorDirection: 'before' | 'after' = 'after', cursor: string | undefined = undefined): Promise<{
documents: AppwriteMovie[],
hasNext: boolean;
}> {
// Get queries from category configuration. Used so this function is generic and can be easily re-used
const queries = category.queries;
const collectionName = category.collectionName ? category.collectionName : "movies";
let documents = [];
// Fetch data with configuration from category
// Limit increased +1 on purpose so we know if there is next page
let response: Models.DocumentList<any> = await sdk.database.listDocuments<AppwriteMovie | AppwriteWatchlist>(collectionName, queries, perPage + 1, undefined, cursor, cursorDirection, category.orderAttributes, category.orderTypes);
// Create clone of documents we got, but depeding on cursor direction, remove additional document we fetched by setting limit to +1
if (cursorDirection === "after") {
documents.push(...response.documents.filter((_d, dIndex) => dIndex < perPage));
} else {
documents.push(...response.documents.filter((_d, dIndex) => dIndex > 0 || response.documents.length === perPage));
}
if (category.collectionName) {
const nestedResponse = await sdk.database.listDocuments<AppwriteMovie>("movies", [
Query.equal("$id", documents.map((d) => d.movieId))
], documents.length);
documents = nestedResponse.documents.map((d) => {
return {
...d,
relationId: response.documents.find((d2) => d2.movieId === d.$id).$id
}
}).sort((a, b) => {
const aIndex = response.documents.findIndex((d) => d.movieId === a.$id);
const bIndex = response.documents.findIndex((d) => d.movieId === b.$id);
return aIndex < bIndex ? -1 : 1;
})
}
// Return documents, but also figure out if there was this +1 document we requested. If yes, there is next page. If not, there is not
return {
documents: documents as AppwriteMovie[],
hasNext: response.documents.length === perPage + 1
};
}
};
Notice that we accept per-page limit and cursor into our function to allow proper pagination. We also return the hasNext
boolean, which says if the next page exists or not. All of that will come into place once we start implementing the movies page, as there we will need this pagination system.
Before we leave our AppwriteService, we implement one more function to allow us to preview movie covers. This one will be similar to the one we created for the trending movie, but we can adjust the configuration to a smaller width, as this will not cover as much of the screen as the trending movie does:
export const AppwriteService = {
// ...
// Generate URL that will resize image to 500px from original potemtially 4k image
// Also, use webp format for better performance
getThumbnail(imageId: string): URL {
return sdk.storage.getFilePreview(imageId, 500, undefined, "top", undefined, undefined, undefined, undefined, undefined, undefined, undefined, "webp");
}
};
Yey, ApprwiteService is ready! π Let's update our movies page in pages/app/movies.vue
, and let's look through app categories, showing movie list for each of them:
<template>
<div>
<div class="flex flex-col space-y-20">
<movie-list
v-for="category in categories"
:key="category.title"
:category="category"
/>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {
AppwriteMovieCategories,
} from '~/services/appwrite'
export default Vue.extend({
data: () => {
return {
categories: AppwriteMovieCategories,
}
},
})
</script>
Now, the complex part... We need to create this <movie-list>
we just used. Such a component should use our AppwriteService to get a list of movies inside the category and manage pagination to allow us to scroll through the category.
First, let's create the component and write HTML that will loop through a list of movies:
<template>
<div>
<h1 class="text-4xl text-zinc-200">{{ category.title }}</h1>
<div
v-if="movies.length > 0"
class="relative grid grid-cols-2 gap-4 mt-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
>
<Movie
v-for="(movie, index) in movies"
:isPaginationEnabled="true"
:onPageChange="onPageChange"
:moviesLength="movies.length"
:isLoading="isLoading"
:isCursorAllowed="isCursorAllowed"
class="col-span-1"
:key="movie.$id"
:appwrite-id="movie.$id"
:movie="movie"
:index="index"
/>
</div>
<div v-if="movies.length <= 0" class="relative mt-6 text-zinc-500">
<p>This list is empty at the moment...</p>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
props: ['category'],
});
</script>
Now, let's implement logic to prepare this movies array:
export default Vue.extend({
// ...
data: () => {
const width = window.innerWidth
let perPage: number
// Depending on the device size, use different page size
if (width < 640) {
perPage = 2
} else if (width < 768) {
perPage = 3
} else if (width < 1024) {
perPage = 4
} else if (width < 1280) {
perPage = 5
} else {
perPage = 6
}
return {
perPage,
isLoading: true,
isBeforeAllowed: false,
isAfterAllowed: true,
movies: [] as AppwriteMovie[],
lastCursor: undefined as undefined | string,
lastDirection: undefined as undefined | 'before' | 'after',
}
},
async created() {
// When component loads, fetch movie list with defaults for pagination (no cursor)
const data = await AppwriteService.getMovies(
this.perPage,
this.$props.category
)
// Store fetched data into component variables
this.movies = data.documents
this.isLoading = false
this.isAfterAllowed = data.hasNext
},
});
Finally, let's add methods that will allow us to paginate over the category:
export default Vue.extend({
// ...
isCursorAllowed(index: number) {
// Simply use variables we fill during fetching data from API
// Depending on index (direction) we want to return different variables
if (index === 0) {
return this.isBeforeAllowed
}
if (index === this.movies.length - 1) {
return this.isAfterAllowed
}
},
async onPageChange(direction: 'before' | 'after') {
// Show spinners instead of arrows
this.isLoading = true
// Use relation ID if provided
const lastRelationId =
direction === 'before'
? this.movies[0].relationId
: this.movies[this.movies.length - 1].relationId
// Depending on direction, get ID of last document we have
let lastId = lastRelationId
? lastRelationId
: direction === 'before'
? this.movies[0].$id
: this.movies[this.movies.length - 1].$id
// Fetch new list of movies using direction and last document ID
const newMovies = await AppwriteService.getMovies(
this.perPage,
this.$props.category,
direction,
lastId
)
// Fetch status if movie is on My List or not
await this.LOAD_FAVOURITE(newMovies.documents.map((d) => d.$id))
// Now lets figure out if we have previous and next page...
// Let's start with saying we have them both, then we will set it to false if we are sure there isnt any
// By setting default to true, we never hide it when we shouldnt.. Worst case scenario, we show it when we shoulding, resulsing in you seing the arrow, but taking no effect and then dissapearing
this.isBeforeAllowed = true
this.isAfterAllowed = true
// If we dont get any documents, it means we got to edge-case when we thought there is next/previous page, but there isnt
if (newMovies.documents.length === 0) {
// Depending on direction, set that arrow to disabled
if (direction === 'before') {
this.isBeforeAllowed = false
} else {
this.isAfterAllowed = false
}
} else {
// If we got some documents, store them to component variable and keep both arrows enabled
this.movies = newMovies.documents
}
// If our Appwrite service says there isn' next page, then...
if (!newMovies.hasNext) {
// Depnding on direction, set that specific direction to disabled
if (direction === 'before') {
this.isBeforeAllowed = false
} else {
this.isAfterAllowed = false
}
}
// Store cursor and direction if I ever need to refresh the current page
this.lastDirection = direction
this.lastCursor = lastId
// Hide spinners, show arrows again
this.isLoading = false
},
});
You can find the whole component in movie list component file.
Woah, that was a ride π₯΅ Let's finish off by creating <Movie>
component in components/Movie.vue
to render one specific movie. We can use movie component file as a reference.
Perfect, we have our movie lists ready! We are missing one last feature to allow users to click a movie to see details. To get that working, you can copy movie modal file, filter modal file, and modal store file. Since these files are only related to HTML, Tailwind CSS, and Vue state management, it would be off-topic to go through them one by one. Don't worry, nothing too interesting is happening in there π
The only missing piece of our puzzle is the watchlist. Let's implement it!
π Watchlist page
As always, let's start by preparing backend communication in our AppwriteService. We will need two functions to update our watchlist - one for removing, one for adding new movies to my watchlist:
export const AppwriteService = {
// ...
async addToMyList(movieId: string): Promise<boolean> {
try {
const { $id: userId } = await sdk.account.get();
await sdk.database.createDocument("watchlists", "unique()", {
userId,
movieId,
createdAt: Math.round(Date.now() / 1000)
});
return true;
} catch (err: any) {
alert(err.message);
return false;
}
},
async deleteFromMyList(movieId: string): Promise<boolean> {
try {
const { $id: userId } = await sdk.account.get();
const watchlistResponse = await sdk.database.listDocuments<AppwriteWatchlist>("watchlists", [
Query.equal("userId", userId),
Query.equal("movieId", movieId)
], 1);
const watchlistId = watchlistResponse.documents[0].$id;
await sdk.database.deleteDocument("watchlists", watchlistId);
return true;
} catch (err: any) {
alert(err.message);
return false;
}
}
};
To achieve proper state management in the future, we need one more function, so when we have a list of movies, we can figure out which ones are already on the user's watchlist:
export const AppwriteService = {
// ...
async getOnlyMyList(movieIds: string[]): Promise<string[]> {
const { $id: userId } = await sdk.account.get();
const watchlistResponse = await sdk.database.listDocuments<AppwriteWatchlist>("watchlists", [
Query.equal("userId", userId),
Query.equal("movieId", movieIds)
], movieIds.length);
return watchlistResponse.documents.map((d) => d.movieId);
}
};
Now, let's create a page /app/my-list
where people can see their watchlist. To do that, we create /pages/app/my-list.vue
file. Thankfully, we can re-use our category logic to render a list of movies properly:
<template>
<div>
<movie-list :category="category" />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { AppwriteCategory } from '../../services/appwrite'
export default Vue.extend({
middleware: 'only-authenticated',
layout: 'app',
data() {
const category: AppwriteCategory = {
collectionName: 'watchlists',
title: 'Movies in My List',
queries: [],
orderAttributes: [],
orderTypes: [],
}
return {
category,
}
},
})
</script>
Then, let's setup state management which will be the source of truth for the whole application about whether the movie is already on the watchlist or not. To do that, we can copy the my list store file from GitHub.
Finally, we define a component that will serve as a button to add/remove the movie from the watchlist. We can find this component in watchlist component file.
Believe it or not, the Netflix clone is ready! π₯³ We should host it so anyone can see it, right?
π Deployment
We will deploy our Nuxt project on Vercel. I fell in love with this platform thanks to the ease of deployment, and the fact this platform is free for pretty-much all of your side projects.
After creating a repository for our project in GitHub, we create a new project on Vercel pointing to this repository. We configure the build process to use npm run generate
for building, dist
as output folder, and npm install
as installation command. We wait for Vercel to finish the build, and we will be presented with a custom Vercel subdomain that contains our website.
When we visit it, we notice we start getting network errors π¬ We look at the console and notice a CORS error from Appwrite... But why? π€
So far, we have only been developing a website locally, meaning we used hostname localhost
. Thankfully, Appwrite allows all communication from localhost
to allow ease of development. Since we are now on Vercel hostname, Appwrite no longer trusts it, and we need to configure this as a production platform. To do it, we visit the Appwrite Console website and enter our project. If we scroll down a little in our dashboard, we will notice the Platforms
section. In here, we need to add a new web platform with the hostname Vercel assigned to you.
After adding the platform, Appwrite now trusts our deployment on Vercel, and we can start using it! π₯³ Believe it or not, we just created a Netflix clone using Appwrite ( Almost ) .
π¨βπ Conclusion
We have successfully cloned Netflix movies using Appwrite. As you can see, your imagination is your limit with Appwrite! To become part of the Appwrite community, you can join our Discord community server. I can't wait to see you around and look at what you build with Appwrite π€©
This project is not over! π With upcoming Appwrite releases, we will be improving this Netflix clone and adding more features. You can get ready for video streaming, custom changes to the backend, and much more!
Here are some handy links and resources:
π Learn more
You can use the following resources to learn more and get help regarding Appwrite and its services
- π Appwrite Github
- π Appwrite Docs
- π¬ Discord Community
Top comments (2)
Part 3: Almost Netflix: A Netflix clone built with Flutter + Appwrite
Thank you for letting me know about the vue.js and iOS app development
theonetechnologies.com/hire-iphone...