Skip to content

Instantly share code, notes, and snippets.

@Meldiron
Created January 19, 2022 10:12
Show Gist options
  • Save Meldiron/4d17f476d00f761c94b8d63dc5b51fcf to your computer and use it in GitHub Desktop.
Save Meldiron/4d17f476d00f761c94b8d63dc5b51fcf to your computer and use it in GitHub Desktop.
Almost Netflix Web - Snippets
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 = {
// ...
};
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
};
}
};
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
},
});
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
},
});
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment