Created
January 19, 2022 10:12
-
-
Save Meldiron/4d17f476d00f761c94b8d63dc5b51fcf to your computer and use it in GitHub Desktop.
Almost Netflix Web - Snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = { | |
// ... | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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