├── amplify ├── backend │ ├── storage │ │ └── netflixmedia │ │ │ ├── storage-params.json │ │ │ ├── parameters.json │ │ │ └── s3-cloudformation-template.json │ ├── api │ │ └── netflix │ │ │ ├── transform.conf.json │ │ │ ├── parameters.json │ │ │ ├── schema.graphql │ │ │ └── stacks │ │ │ └── CustomResources.json │ ├── backend-config.json │ └── auth │ │ └── netflixd3938a31 │ │ ├── parameters.json │ │ └── netflixd3938a31-cloudformation-template.yml ├── .config │ └── project-config.json └── team-provider-info.json ├── screens ├── HomeScreen │ ├── index.tsx │ ├── styles.ts │ └── HomeScreen.tsx ├── NotFoundScreen.tsx ├── TabTwoScreen.tsx └── MovieDetailsScreen │ ├── styles.ts │ └── index.tsx ├── assets ├── images │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ └── adaptive-icon.png ├── fonts │ └── SpaceMono-Regular.ttf └── data │ ├── movie.ts │ └── categories.ts ├── src ├── models │ ├── schema.d.ts │ ├── index.js │ ├── index.d.ts │ └── schema.js ├── graphql │ ├── queries.ts │ ├── subscriptions.ts │ └── mutations.ts └── API.ts ├── babel.config.js ├── amplify.json ├── components ├── StyledText.tsx ├── VideoPlayer │ ├── styles.ts │ └── index.tsx ├── __tests__ │ └── StyledText-test.js ├── MovieItem │ ├── styles.ts │ └── index.tsx ├── HomeCategory │ ├── styles.ts │ └── index.tsx ├── EpisodeItem │ ├── styles.ts │ └── index.tsx ├── Themed.tsx └── EditScreenInfo.tsx ├── hooks ├── useColorScheme.web.ts ├── useColorScheme.ts └── useCachedResources.ts ├── constants ├── Layout.ts └── Colors.ts ├── tsconfig.json ├── .expo-shared └── assets.json ├── .graphqlconfig.yml ├── navigation ├── LinkingConfiguration.ts ├── index.tsx └── BottomTabNavigator.tsx ├── .gitignore ├── types.tsx ├── app.json ├── App.tsx ├── LICENSE └── package.json /amplify/backend/storage/netflixmedia/storage-params.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /screens/HomeScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import HomeScreen from './HomeScreen'; 2 | 3 | export default HomeScreen; -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /src/models/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@aws-amplify/datastore'; 2 | 3 | export declare const schema: Schema; -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/NetflixClone/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /amplify.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": 3 | { 4 | "graphqltransformer": 5 | { 6 | "transformerversion": 5 7 | }, 8 | "keytransformer": 9 | { 10 | "defaultquery": true 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /screens/HomeScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | padding: 20, 7 | }, 8 | }); 9 | 10 | export default styles; -------------------------------------------------------------------------------- /components/StyledText.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Text, TextProps } from './Themed'; 4 | 5 | export function MonoText(props: TextProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /components/VideoPlayer/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | video: { 5 | width: '100%', 6 | aspectRatio: 16/9, 7 | 8 | } 9 | }); 10 | 11 | export default styles; -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // useColorScheme from react-native does not support web currently. You can replace 2 | // this with react-native-appearance if you would like theme support on web. 3 | export default function useColorScheme() { 4 | return 'light'; 5 | } -------------------------------------------------------------------------------- /amplify/backend/api/netflix/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true, 4 | "ResolverConfig": { 5 | "project": { 6 | "ConflictHandler": "AUTOMERGE", 7 | "ConflictDetection": "VERSION" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { initSchema } from '@aws-amplify/datastore'; 3 | import { schema } from './schema'; 4 | 5 | 6 | 7 | const { Category, Movie, Season, Episode } = initSchema(schema); 8 | 9 | export { 10 | Category, 11 | Movie, 12 | Season, 13 | Episode 14 | }; -------------------------------------------------------------------------------- /constants/Layout.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const width = Dimensions.get('window').width; 4 | const height = Dimensions.get('window').height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true, 3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true, 4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 6 | } 7 | -------------------------------------------------------------------------------- /components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import { MonoText } from '../StyledText'; 5 | 6 | it(`renders correctly`, () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /amplify/backend/api/netflix/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "netflix", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authnetflixd3938a31", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | netflix: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.ts 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: typescript 11 | generatedFileName: src/API.ts 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { ColorSchemeName, useColorScheme as _useColorScheme } from 'react-native'; 2 | 3 | // The useColorScheme value is always either light or dark, but the built-in 4 | // type suggests that it can be null. This will not happen in practice, so this 5 | // makes it a bit easier to work with. 6 | export default function useColorScheme(): NonNullable { 7 | return _useColorScheme() as NonNullable; 8 | } 9 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | const tintColorLight = '#2f95dc'; 2 | const tintColorDark = '#fff'; 3 | 4 | export default { 5 | light: { 6 | text: '#000', 7 | background: '#fff', 8 | tint: tintColorLight, 9 | tabIconDefault: '#ccc', 10 | tabIconSelected: tintColorLight, 11 | }, 12 | dark: { 13 | text: '#fff', 14 | background: '#000', 15 | tint: tintColorDark, 16 | tabIconDefault: '#ccc', 17 | tabIconSelected: tintColorDark, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /components/MovieItem/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | padding: 20, 7 | }, 8 | image: { 9 | width: 100, 10 | height: 170, 11 | resizeMode: 'cover', 12 | borderRadius: 5, 13 | margin: 5, 14 | }, 15 | title: { 16 | fontSize: 20, 17 | fontWeight: 'bold', 18 | } 19 | }); 20 | 21 | export default styles; -------------------------------------------------------------------------------- /components/HomeCategory/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | padding: 20, 7 | }, 8 | image: { 9 | width: 100, 10 | height: 170, 11 | resizeMode: 'cover', 12 | borderRadius: 5, 13 | margin: 5, 14 | }, 15 | title: { 16 | fontSize: 20, 17 | fontWeight: 'bold', 18 | } 19 | }); 20 | 21 | export default styles; -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Netflix", 3 | "version": "3.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react-native", 7 | "config": { 8 | "SourceDir": "/", 9 | "DistributionDir": "/", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | import * as Linking from 'expo-linking'; 2 | 3 | export default { 4 | prefixes: [Linking.makeUrl('/')], 5 | config: { 6 | screens: { 7 | Root: { 8 | screens: { 9 | TabOne: { 10 | screens: { 11 | TabOneScreen: 'one', 12 | }, 13 | }, 14 | TabTwo: { 15 | screens: { 16 | TabTwoScreen: 'two', 17 | }, 18 | }, 19 | }, 20 | }, 21 | NotFound: '*', 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | 15 | #amplify 16 | amplify/\#current-cloud-backend 17 | amplify/.config/local-* 18 | amplify/mock-data 19 | amplify/backend/amplify-meta.json 20 | amplify/backend/awscloudformation 21 | build/ 22 | dist/ 23 | node_modules/ 24 | aws-exports.js 25 | awsconfiguration.json 26 | amplifyconfiguration.json 27 | amplify-build-config.json 28 | amplify-gradle-config.json 29 | amplifytools.xcconfig -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | export type RootStackParamList = { 2 | Root: undefined; 3 | NotFound: undefined; 4 | }; 5 | 6 | export type BottomTabParamList = { 7 | Home: undefined; 8 | Coming_Soon: undefined; 9 | Search: undefined; 10 | Downloads: undefined; 11 | }; 12 | 13 | export type HomeParamList = { 14 | HomeScreen: undefined; 15 | MovieDetailsScreen: undefined; 16 | }; 17 | 18 | export type TabTwoParamList = { 19 | TabTwoScreen: undefined; 20 | }; 21 | 22 | export type Episode = { 23 | id: string, 24 | title: string, 25 | poster: string, 26 | duration: string, 27 | plot: string, 28 | video: string, 29 | } 30 | -------------------------------------------------------------------------------- /components/EpisodeItem/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | row: { 5 | flexDirection: 'row', 6 | justifyContent: 'space-between', 7 | alignItems: 'center', 8 | marginBottom: 5, 9 | }, 10 | image: { 11 | height: 75, 12 | aspectRatio: 16/9, 13 | resizeMode: 'cover', 14 | borderRadius: 3, 15 | }, 16 | titleContainer: { 17 | flex: 1, 18 | padding: 5, 19 | justifyContent: 'center', 20 | }, 21 | title: { 22 | 23 | }, 24 | duration: { 25 | color: 'darkgrey', 26 | fontSize: 10, 27 | }, 28 | plot: { 29 | color: 'darkgrey' 30 | } 31 | }) 32 | 33 | export default styles; -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Netflix", 4 | "slug": "Netflix", 5 | "version": "1.0.0", 6 | "icon": "./assets/images/icon.png", 7 | "scheme": "myapp", 8 | "userInterfaceStyle": "dark", 9 | "splash": { 10 | "image": "./assets/images/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/images/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/images/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "netflixd3938a31": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [], 7 | "customAuth": false 8 | } 9 | }, 10 | "api": { 11 | "netflix": { 12 | "service": "AppSync", 13 | "providerPlugin": "awscloudformation", 14 | "output": { 15 | "authConfig": { 16 | "defaultAuthentication": { 17 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 18 | "userPoolConfig": { 19 | "userPoolId": "authnetflixd3938a31_userpool_d3938a31" 20 | } 21 | }, 22 | "additionalAuthenticationProviders": [ 23 | { 24 | "authenticationType": "AWS_IAM" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | }, 31 | "storage": { 32 | "netflixmedia": { 33 | "service": "S3", 34 | "providerPlugin": "awscloudformation" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import React from 'react'; 3 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 4 | import Amplify from 'aws-amplify'; 5 | import { withAuthenticator } from 'aws-amplify-react-native'; 6 | 7 | import useCachedResources from './hooks/useCachedResources'; 8 | import useColorScheme from './hooks/useColorScheme'; 9 | import Navigation from './navigation'; 10 | import config from './aws-exports'; 11 | 12 | Amplify.configure(config); 13 | 14 | function App() { 15 | const isLoadingComplete = useCachedResources(); 16 | const colorScheme = useColorScheme(); 17 | 18 | if (!isLoadingComplete) { 19 | return null; 20 | } else { 21 | return ( 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default withAuthenticator(App); 31 | -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-netflix-dev-151224-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::704219588443:role/amplify-netflix-dev-151224-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::704219588443:role/amplify-netflix-dev-151224-authRole", 7 | "Region": "eu-west-1", 8 | "DeploymentBucketName": "amplify-netflix-dev-151224-deployment", 9 | "UnauthRoleName": "amplify-netflix-dev-151224-unauthRole", 10 | "StackName": "amplify-netflix-dev-151224", 11 | "StackId": "arn:aws:cloudformation:eu-west-1:704219588443:stack/amplify-netflix-dev-151224/850eb300-88c5-11eb-ae05-06fe7da2d029", 12 | "AmplifyAppId": "d29vqtvfcgauub" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "netflixd3938a31": {} 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /screens/HomeScreen/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Image, FlatList } from 'react-native'; 3 | import { DataStore } from 'aws-amplify'; 4 | 5 | import { Text, View } from '../../components/Themed'; 6 | 7 | import styles from './styles'; 8 | import HomeCategory from '../../components/HomeCategory'; 9 | import { Category } from '../../src/models' 10 | 11 | const HomeScreen = () => { 12 | const [categories, setCategories] = useState([]); 13 | 14 | useEffect(() => { 15 | const fetchCategories = async () => { 16 | setCategories(await DataStore.query(Category)); 17 | 18 | }; 19 | fetchCategories(); 20 | }, []); 21 | 22 | return ( 23 | 24 | {/* List of categories */} 25 | } 28 | /> 29 | 30 | ); 31 | } 32 | 33 | export default HomeScreen; 34 | -------------------------------------------------------------------------------- /components/MovieItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState} from 'react' 2 | import { Pressable, Image } from 'react-native' 3 | import { useNavigation } from '@react-navigation/native'; 4 | import { Storage } from 'aws-amplify'; 5 | 6 | import { Movie } from '../../src/models'; 7 | import styles from './styles'; 8 | 9 | const MovieItem = ({ movie }: {movie: Movie}) => { 10 | const navigation = useNavigation(); 11 | const [imageUrl, setImageUrl] = useState(''); 12 | 13 | const onMoviePress = () => { 14 | navigation.navigate('MovieDetailsScreen', { id: movie.id }) 15 | } 16 | 17 | useEffect(() => { 18 | if (movie.poster.startsWith('http')) { 19 | setImageUrl(movie.poster); 20 | return; 21 | } 22 | 23 | Storage.get(movie.poster).then(setImageUrl) 24 | }, []) 25 | 26 | return ( 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default MovieItem 34 | -------------------------------------------------------------------------------- /hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons'; 2 | import * as Font from 'expo-font'; 3 | import * as SplashScreen from 'expo-splash-screen'; 4 | import * as React from 'react'; 5 | 6 | export default function useCachedResources() { 7 | const [isLoadingComplete, setLoadingComplete] = React.useState(false); 8 | 9 | // Load any resources or data that we need prior to rendering the app 10 | React.useEffect(() => { 11 | async function loadResourcesAndDataAsync() { 12 | try { 13 | SplashScreen.preventAutoHideAsync(); 14 | 15 | // Load fonts 16 | await Font.loadAsync({ 17 | ...Ionicons.font, 18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'), 19 | }); 20 | } catch (e) { 21 | // We might want to provide this error information to an error reporting service 22 | console.warn(e); 23 | } finally { 24 | setLoadingComplete(true); 25 | SplashScreen.hideAsync(); 26 | } 27 | } 28 | 29 | loadResourcesAndDataAsync(); 30 | }, []); 31 | 32 | return isLoadingComplete; 33 | } 34 | -------------------------------------------------------------------------------- /amplify/backend/api/netflix/schema.graphql: -------------------------------------------------------------------------------- 1 | type Category @model { 2 | id: ID! 3 | title: String! 4 | 5 | movies: [Movie] @connection(keyName: "byCategory", fields: ["id"]) 6 | } 7 | 8 | type Movie @model 9 | @key(name: "byCategory", fields: ["categoryID"]){ 10 | id: ID! 11 | title: String! 12 | poster: String! 13 | year: Int 14 | numberOfSeasons: Int 15 | 16 | plot: String 17 | cast: String 18 | creator: String 19 | 20 | categoryID: ID! 21 | 22 | seasons: [Season] @connection(keyName: "byMovie", fields: ["id"]) 23 | } 24 | 25 | type Season @model 26 | @key(name: "byMovie", fields: ["movieID"]){ 27 | id: ID! 28 | name: String! 29 | 30 | movieID: ID! 31 | movie: Movie @connection(fields: ["movieID"]) 32 | 33 | episodes: [Episode] @connection(keyName: "bySeason", fields: ["id"]) 34 | } 35 | 36 | type Episode @model 37 | @key(name: "bySeason", fields: ["seasonID"]) 38 | { 39 | id: ID! 40 | title: String! 41 | poster: String! 42 | duration: String! 43 | plot: String 44 | video: String! 45 | 46 | seasonID: ID! 47 | season: Season @connection(fields: ["seasonID"]) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vadim Savin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /screens/NotFoundScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | import * as React from 'react'; 3 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 4 | 5 | import { RootStackParamList } from '../types'; 6 | 7 | export default function NotFoundScreen({ 8 | navigation, 9 | }: StackScreenProps) { 10 | return ( 11 | 12 | This screen doesn't exist. 13 | navigation.replace('Root')} style={styles.link}> 14 | Go to home screen! 15 | 16 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | backgroundColor: '#fff', 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | title: { 29 | fontSize: 20, 30 | fontWeight: 'bold', 31 | }, 32 | link: { 33 | marginTop: 15, 34 | paddingVertical: 15, 35 | }, 36 | linkText: { 37 | fontSize: 14, 38 | color: '#2e78b7', 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /screens/TabTwoScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Pressable, StyleSheet } from 'react-native'; 3 | import { Auth } from 'aws-amplify'; 4 | 5 | import EditScreenInfo from '../components/EditScreenInfo'; 6 | import { Text, View } from '../components/Themed'; 7 | 8 | export default function TabTwoScreen() { 9 | const onLogout = () => { 10 | Auth.signOut(); 11 | } 12 | 13 | return ( 14 | 15 | Tab Two 16 | 17 | 18 | 19 | Logout 20 | 21 | 22 | ); 23 | } 24 | 25 | const styles = StyleSheet.create({ 26 | container: { 27 | flex: 1, 28 | alignItems: 'center', 29 | justifyContent: 'center', 30 | }, 31 | title: { 32 | fontSize: 20, 33 | fontWeight: 'bold', 34 | }, 35 | separator: { 36 | marginVertical: 30, 37 | height: 1, 38 | width: '80%', 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /amplify/backend/storage/netflixmedia/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucketName": "netflix49b040bd779644519ba8d91c478e9c9a", 3 | "authPolicyName": "s3_amplify_36a03f8a", 4 | "unauthPolicyName": "s3_amplify_36a03f8a", 5 | "authRoleName": { 6 | "Ref": "AuthRoleName" 7 | }, 8 | "unauthRoleName": { 9 | "Ref": "UnauthRoleName" 10 | }, 11 | "selectedGuestPermissions": [ 12 | "s3:GetObject", 13 | "s3:ListBucket" 14 | ], 15 | "selectedAuthenticatedPermissions": [ 16 | "s3:GetObject", 17 | "s3:ListBucket" 18 | ], 19 | "s3PermissionsAuthenticatedPublic": "s3:GetObject", 20 | "s3PublicPolicy": "Public_policy_b6db8f9f", 21 | "s3PermissionsAuthenticatedUploads": "DISALLOW", 22 | "s3UploadsPolicy": "Uploads_policy_b6db8f9f", 23 | "s3PermissionsAuthenticatedProtected": "s3:GetObject", 24 | "s3ProtectedPolicy": "Protected_policy_b6db8f9f", 25 | "s3PermissionsAuthenticatedPrivate": "s3:GetObject", 26 | "s3PrivatePolicy": "Private_policy_b6db8f9f", 27 | "AuthenticatedAllowList": "ALLOW", 28 | "s3ReadPolicy": "read_policy_b6db8f9f", 29 | "s3PermissionsGuestPublic": "DISALLOW", 30 | "s3PermissionsGuestUploads": "DISALLOW", 31 | "GuestAllowList": "DISALLOW", 32 | "triggerFunction": "NONE" 33 | } -------------------------------------------------------------------------------- /components/HomeCategory/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Image, FlatList, Pressable } from 'react-native'; 3 | import { Text } from '../../components/Themed'; 4 | import MovieItem from '../../components/MovieItem'; 5 | import { Storage } from 'aws-amplify'; 6 | 7 | import styles from './styles'; 8 | import { Category, Movie } from '../../src/models'; 9 | import { DataStore } from '@aws-amplify/datastore'; 10 | 11 | interface HomeCategoryProps { 12 | category: Category, 13 | } 14 | 15 | const HomeCategory = (props: HomeCategoryProps) => { 16 | const { category } = props; 17 | 18 | const [movies, setMovies] = useState([]); 19 | 20 | 21 | useEffect(() => { 22 | const fetchMovies = async () => { 23 | const result = (await DataStore.query(Movie)) 24 | .filter((movie) => movie.categoryID === category.id) 25 | setMovies(result); 26 | }; 27 | 28 | fetchMovies(); 29 | }, []) 30 | 31 | return ( 32 | <> 33 | {category.title} 34 | } 37 | horizontal 38 | showsHorizontalScrollIndicator={false} 39 | /> 40 | 41 | ); 42 | } 43 | 44 | export default HomeCategory; 45 | -------------------------------------------------------------------------------- /components/Themed.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text as DefaultText, View as DefaultView } from 'react-native'; 3 | 4 | import Colors from '../constants/Colors'; 5 | import useColorScheme from '../hooks/useColorScheme'; 6 | 7 | export function useThemeColor( 8 | props: { light?: string; dark?: string }, 9 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 10 | ) { 11 | const theme = useColorScheme(); 12 | const colorFromProps = props[theme]; 13 | 14 | if (colorFromProps) { 15 | return colorFromProps; 16 | } else { 17 | return Colors[theme][colorName]; 18 | } 19 | } 20 | 21 | type ThemeProps = { 22 | lightColor?: string; 23 | darkColor?: string; 24 | }; 25 | 26 | export type TextProps = ThemeProps & DefaultText['props']; 27 | export type ViewProps = ThemeProps & DefaultView['props']; 28 | 29 | export function Text(props: TextProps) { 30 | const { style, lightColor, darkColor, ...otherProps } = props; 31 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 32 | 33 | return ; 34 | } 35 | 36 | export function View(props: ViewProps) { 37 | const { style, lightColor, darkColor, ...otherProps } = props; 38 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 39 | 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import * as React from 'react'; 4 | import { ColorSchemeName } from 'react-native'; 5 | 6 | import NotFoundScreen from '../screens/NotFoundScreen'; 7 | import { RootStackParamList } from '../types'; 8 | import BottomTabNavigator from './BottomTabNavigator'; 9 | import LinkingConfiguration from './LinkingConfiguration'; 10 | 11 | // If you are not familiar with React Navigation, we recommend going through the 12 | // "Fundamentals" guide: https://reactnavigation.org/docs/getting-started 13 | export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) { 14 | return ( 15 | 18 | 19 | 20 | ); 21 | } 22 | 23 | // A root stack navigator is often used for displaying modals on top of all other content 24 | // Read more here: https://reactnavigation.org/docs/modal 25 | const Stack = createStackNavigator(); 26 | 27 | function RootNavigator() { 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /screens/MovieDetailsScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | image: { 5 | width: '100%', 6 | aspectRatio: 16/9, 7 | resizeMode: 'cover', 8 | }, 9 | title: { 10 | fontSize: 24, 11 | fontWeight: 'bold' 12 | }, 13 | match: { 14 | color: '#59d467', 15 | fontWeight: 'bold', 16 | marginRight: 5, 17 | }, 18 | year: { 19 | color: '#757575', 20 | marginRight: 5, 21 | }, 22 | ageContainer: { 23 | backgroundColor: '#e6e229', 24 | justifyContent: 'center', 25 | alignItems: 'center', 26 | borderRadius: 2, 27 | paddingHorizontal: 3, 28 | marginRight: 5, 29 | }, 30 | age: { 31 | color: 'black', 32 | fontWeight: 'bold' 33 | }, 34 | 35 | // Button 36 | playButton: { 37 | backgroundColor: 'white', 38 | justifyContent: 'center', 39 | alignItems: 'center', 40 | padding: 5, 41 | borderRadius: 3, 42 | marginVertical: 5, 43 | }, 44 | playButtonText: { 45 | color: 'black', 46 | fontSize: 16, 47 | fontWeight: 'bold' 48 | }, 49 | downloadButton: { 50 | backgroundColor: '#2b2b2b', 51 | justifyContent: 'center', 52 | alignItems: 'center', 53 | padding: 5, 54 | borderRadius: 3, 55 | marginVertical: 5, 56 | }, 57 | downloadButtonText: { 58 | color: 'white', 59 | fontSize: 16, 60 | fontWeight: 'bold' 61 | } 62 | }) 63 | 64 | export default styles; 65 | -------------------------------------------------------------------------------- /components/EpisodeItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Image, Pressable } from 'react-native'; 3 | import { AntDesign } from '@expo/vector-icons'; 4 | import { Text, View } from '../../components/Themed'; 5 | import styles from './styles'; 6 | import { Episode } from '../../types'; 7 | 8 | // { 9 | // id: 'episode1', 10 | // title: '1. Pilot Part 1 & 2', 11 | // poster: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/netflix/ep0.jpg', 12 | // duration: '1h 21m', 13 | // plot: 'When Harvey\'s promotion requires him to recruit and hire a graduate of Harvard Law, he chooses Mike Ross. But Mike doesn\'t actualy have a law degree', 14 | // video: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4', 15 | // } 16 | 17 | interface EpisodeItemProps { 18 | episode: Episode; 19 | onPress: (eppisode: Episode) => {} 20 | } 21 | 22 | const EpisodeItem = (props: EpisodeItemProps) => { 23 | const { episode, onPress } = props; 24 | 25 | return ( 26 | onPress(episode)}> 27 | 28 | 29 | 30 | 31 | {episode.title} 32 | {episode.duration} 33 | 34 | 35 | 36 | 37 | 38 | {episode.plot} 39 | 40 | ) 41 | }; 42 | 43 | export default EpisodeItem; 44 | 45 | -------------------------------------------------------------------------------- /src/models/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ModelInit, MutableModel, PersistentModelConstructor } from "@aws-amplify/datastore"; 2 | 3 | 4 | 5 | 6 | 7 | export declare class Category { 8 | readonly id: string; 9 | readonly title: string; 10 | readonly movies?: Movie[]; 11 | constructor(init: ModelInit); 12 | static copyOf(source: Category, mutator: (draft: MutableModel) => MutableModel | void): Category; 13 | } 14 | 15 | export declare class Movie { 16 | readonly id: string; 17 | readonly title: string; 18 | readonly poster: string; 19 | readonly year?: number; 20 | readonly numberOfSeasons?: number; 21 | readonly plot?: string; 22 | readonly cast?: string; 23 | readonly creator?: string; 24 | readonly categoryID: string; 25 | readonly seasons?: Season[]; 26 | constructor(init: ModelInit); 27 | static copyOf(source: Movie, mutator: (draft: MutableModel) => MutableModel | void): Movie; 28 | } 29 | 30 | export declare class Season { 31 | readonly id: string; 32 | readonly name: string; 33 | readonly movie?: Movie; 34 | readonly episodes?: Episode[]; 35 | constructor(init: ModelInit); 36 | static copyOf(source: Season, mutator: (draft: MutableModel) => MutableModel | void): Season; 37 | } 38 | 39 | export declare class Episode { 40 | readonly id: string; 41 | readonly title: string; 42 | readonly poster: string; 43 | readonly duration: string; 44 | readonly plot?: string; 45 | readonly video: string; 46 | readonly season?: Season; 47 | constructor(init: ModelInit); 48 | static copyOf(source: Episode, mutator: (draft: MutableModel) => MutableModel | void): Episode; 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject", 9 | "test": "jest --watchAll" 10 | }, 11 | "jest": { 12 | "preset": "jest-expo" 13 | }, 14 | "dependencies": { 15 | "@expo/vector-icons": "^12.0.0", 16 | "@react-native-community/masked-view": "0.1.10", 17 | "@react-native-community/netinfo": "^6.0.0", 18 | "@react-native-picker/picker": "1.9.2", 19 | "@react-navigation/bottom-tabs": "5.11.2", 20 | "@react-navigation/native": "~5.8.10", 21 | "@react-navigation/stack": "~5.12.8", 22 | "amazon-cognito-identity-js": "^4.6.0", 23 | "aws-amplify": "^3.3.25", 24 | "aws-amplify-react-native": "^4.3.2", 25 | "expo": "~40.0.0", 26 | "expo-asset": "~8.2.1", 27 | "expo-av": "~8.7.0", 28 | "expo-constants": "~9.3.0", 29 | "expo-font": "~8.4.0", 30 | "expo-linking": "~2.0.0", 31 | "expo-splash-screen": "~0.8.0", 32 | "expo-status-bar": "~1.0.3", 33 | "expo-web-browser": "~8.6.0", 34 | "react": "16.13.1", 35 | "react-dom": "16.13.1", 36 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz", 37 | "react-native-gesture-handler": "~1.8.0", 38 | "react-native-safe-area-context": "3.1.9", 39 | "react-native-screens": "~2.15.0", 40 | "react-native-web": "~0.13.12" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "~7.9.0", 44 | "@types/react": "~16.9.35", 45 | "@types/react-native": "~0.63.2", 46 | "jest-expo": "~40.0.0", 47 | "typescript": "~4.0.0" 48 | }, 49 | "private": true 50 | } 51 | -------------------------------------------------------------------------------- /amplify/backend/auth/netflixd3938a31/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "netflixd3938a31_identitypool_d3938a31", 3 | "allowUnauthenticatedIdentities": false, 4 | "resourceNameTruncated": "netflid3938a31", 5 | "userPoolName": "netflixd3938a31_userpool_d3938a31", 6 | "autoVerifiedAttributes": [ 7 | "email" 8 | ], 9 | "mfaConfiguration": "OFF", 10 | "mfaTypes": [ 11 | "SMS Text Message" 12 | ], 13 | "smsAuthenticationMessage": "Your authentication code is {####}", 14 | "smsVerificationMessage": "Your verification code is {####}", 15 | "emailVerificationSubject": "Your verification code", 16 | "emailVerificationMessage": "Your verification code is {####}", 17 | "defaultPasswordPolicy": false, 18 | "passwordPolicyMinLength": 8, 19 | "passwordPolicyCharacters": [], 20 | "requiredAttributes": [ 21 | "email" 22 | ], 23 | "userpoolClientGenerateSecret": true, 24 | "userpoolClientRefreshTokenValidity": 30, 25 | "userpoolClientWriteAttributes": [ 26 | "email" 27 | ], 28 | "userpoolClientReadAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientLambdaRole": "netflid3938a31_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "sharedId": "d3938a31", 34 | "resourceName": "netflixd3938a31", 35 | "authSelections": "identityPoolAndUserPool", 36 | "authRoleArn": { 37 | "Fn::GetAtt": [ 38 | "AuthRole", 39 | "Arn" 40 | ] 41 | }, 42 | "unauthRoleArn": { 43 | "Fn::GetAtt": [ 44 | "UnauthRole", 45 | "Arn" 46 | ] 47 | }, 48 | "useDefault": "default", 49 | "userPoolGroupList": [], 50 | "dependsOn": [] 51 | } -------------------------------------------------------------------------------- /amplify/backend/api/netflix/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": ["true", "false"] 50 | } 51 | }, 52 | "Outputs": { 53 | "EmptyOutput": { 54 | "Description": "An empty output. You may delete this if you have at least one resource above.", 55 | "Value": "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/VideoPlayer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react' 2 | import { View, Text } from 'react-native' 3 | import { Video } from 'expo-av'; 4 | import { Storage } from 'aws-amplify'; 5 | import { Episode } from '../../types'; 6 | import styles from './styles'; 7 | import { Playback } from 'expo-av/build/AV'; 8 | 9 | interface VideoPlayerProps { 10 | episode: Episode; 11 | } 12 | 13 | const VideoPlayer = (props: VideoPlayerProps) => { 14 | const { episode } = props; 15 | const [videoURL, setVideoURL] = useState(''); 16 | 17 | const [status, setStatus] = useState({}); 18 | const video = useRef(null); 19 | 20 | useEffect(() => { 21 | if (episode.video.startsWith('http')) { 22 | setVideoURL(episode.video); 23 | return; 24 | } 25 | Storage.get(episode.video).then(setVideoURL); 26 | }, [episode]) 27 | 28 | useEffect(() => { 29 | if (!video) { 30 | return; 31 | } 32 | (async () => { 33 | await video?.current?.unloadAsync(); 34 | await video?.current?.loadAsync( 35 | { uri: videoURL }, 36 | {}, 37 | false 38 | ); 39 | })(); 40 | }, [videoURL]) 41 | 42 | console.log(videoURL); 43 | 44 | if (videoURL === '') { 45 | return null; 46 | } 47 | 48 | return ( 49 |