├── .eslintignore ├── amplify ├── backend │ ├── storage │ │ └── youtubeclonemedia │ │ │ ├── storage-params.json │ │ │ ├── parameters.json │ │ │ └── s3-cloudformation-template.json │ ├── tags.json │ ├── api │ │ └── YoutubeClone │ │ │ ├── transform.conf.json │ │ │ ├── parameters.json │ │ │ ├── resolvers │ │ │ └── README.md │ │ │ ├── schema.graphql │ │ │ └── stacks │ │ │ └── CustomResources.json │ ├── analytics │ │ └── youtubeclone │ │ │ ├── parameters.json │ │ │ └── pinpoint-cloudformation-template.json │ ├── backend-config.json │ └── auth │ │ └── YoutubeClone │ │ ├── parameters.json │ │ └── YoutubeClone-cloudformation-template.yml ├── .config │ └── project-config.json ├── team-provider-info.json └── cli.json ├── screens ├── VideoScreen │ ├── index.ts │ ├── styles.ts │ └── VideoScreen.tsx ├── HomeScreen.tsx ├── TabOneScreen.tsx ├── NotFoundScreen.tsx ├── TabTwoScreen.tsx └── VideoUploadScreen.tsx ├── assets ├── images │ ├── icon.png │ ├── logo.png │ ├── splash.png │ ├── favicon.png │ └── adaptive-icon.png ├── fonts │ └── SpaceMono-Regular.ttf └── data │ ├── video.json │ ├── videos.json │ └── comments.json ├── components ├── VideoListItem │ ├── index.tsx │ ├── styles.ts │ └── VideoListItem.tsx ├── StyledText.tsx ├── __tests__ │ └── StyledText-test.js ├── VideoPlayer │ └── index.tsx ├── VideoComment │ └── index.tsx ├── Themed.tsx ├── VideoComments │ └── index.tsx └── EditScreenInfo.tsx ├── src └── models │ ├── schema.d.ts │ ├── index.js │ ├── index.d.ts │ └── schema.js ├── tsconfig.json ├── babel.config.js ├── constants ├── Layout.ts └── Colors.ts ├── .expo-shared └── assets.json ├── .vscode └── settings.json ├── hooks ├── useColorScheme.ts └── useCachedResources.ts ├── types.tsx ├── .gitignore ├── navigation ├── LinkingConfiguration.ts ├── index.tsx ├── HomeStack.tsx └── BottomTabNavigator.tsx ├── app.json ├── LICENSE ├── App.tsx └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | src/models -------------------------------------------------------------------------------- /amplify/backend/storage/youtubeclonemedia/storage-params.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /screens/VideoScreen/index.ts: -------------------------------------------------------------------------------- 1 | import VideoScreen from './VideoScreen'; 2 | export default VideoScreen; -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/YoutubeClone/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/YoutubeClone/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/YoutubeClone/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /components/VideoListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import VideoListItem from './VideoListItem'; 2 | export default VideoListItem; -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/YoutubeClone/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /src/models/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@aws-amplify/datastore'; 2 | 3 | export declare const schema: Schema; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/YoutubeClone/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/YoutubeClone/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 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { initSchema } from '@aws-amplify/datastore'; 3 | import { schema } from './schema'; 4 | 5 | 6 | 7 | const { Comment, User, Video } = initSchema(schema); 8 | 9 | export { 10 | Comment, 11 | User, 12 | Video 13 | }; -------------------------------------------------------------------------------- /amplify/backend/api/YoutubeClone/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true, 4 | "ResolverConfig": { 5 | "project": { 6 | "ConflictHandler": "AUTOMERGE", 7 | "ConflictDetection": "VERSION" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /amplify/backend/api/YoutubeClone/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "YoutubeClone", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authYoutubeClone", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true, 3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true, 4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 6 | } 7 | -------------------------------------------------------------------------------- /amplify/backend/api/YoutubeClone/resolvers/README.md: -------------------------------------------------------------------------------- 1 | Any resolvers that you add in this directory will override the ones automatically generated by Amplify CLI and will be directly copied to the cloud. 2 | For more information, visit [https://docs.amplify.aws/cli/graphql-transformer/resolvers](https://docs.amplify.aws/cli/graphql-transformer/resolvers) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "amplify/.config": true, 4 | "amplify/**/*-parameters.json": true, 5 | "amplify/**/amplify.state": true, 6 | "amplify/**/transform.conf.json": true, 7 | "amplify/#current-cloud-backend": true, 8 | "amplify/backend/amplify-meta.json": true, 9 | "amplify/backend/awscloudformation": true 10 | } 11 | } -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": [ 3 | "awscloudformation" 4 | ], 5 | "projectName": "YoutubeClone", 6 | "version": "3.1", 7 | "frontend": "javascript", 8 | "javascript": { 9 | "framework": "react-native", 10 | "config": { 11 | "SourceDir": "src", 12 | "DistributionDir": "/", 13 | "BuildCommand": "npm run-script build", 14 | "StartCommand": "npm run-script start" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about using TypeScript with React Navigation: 3 | * https://reactnavigation.org/docs/typescript/ 4 | */ 5 | 6 | export type RootStackParamList = { 7 | Root: undefined; 8 | VideoScreen: undefined; 9 | NotFound: undefined; 10 | }; 11 | 12 | export type BottomTabParamList = { 13 | Home: undefined; 14 | Explore: undefined; 15 | New: undefined; 16 | Subscriptions: undefined; 17 | Library: undefined; 18 | }; 19 | 20 | export type TabOneParamList = { 21 | TabOneScreen: undefined; 22 | }; 23 | 24 | export type TabTwoParamList = { 25 | TabTwoScreen: undefined; 26 | }; 27 | -------------------------------------------------------------------------------- /.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/logs 19 | amplify/mock-data 20 | amplify/backend/amplify-meta.json 21 | amplify/backend/awscloudformation 22 | amplify/backend/.temp 23 | build/ 24 | dist/ 25 | node_modules/ 26 | aws-exports.js 27 | awsconfiguration.json 28 | amplifyconfiguration.json 29 | amplifyconfiguration.dart 30 | amplify-build-config.json 31 | amplify-gradle-config.json 32 | amplifytools.xcconfig 33 | .secret-* -------------------------------------------------------------------------------- /assets/data/video.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "createdAt": "5 months ago", 4 | "title": "Build a Realtime Chat App in React Native (tutorial for beginners) 🔴 ", 5 | "thumbnail": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/thumbnails/thumbnail1.jpeg", 6 | "videoUrl": "http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4", 7 | "duration": 384, 8 | "user": { 9 | "name": "Vadim Savin", 10 | "image": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg", 11 | "subscribers": 100000 12 | }, 13 | "views": 357000, 14 | "tags": "#VadimSavin #notjust #notJustDeveloper", 15 | "likes": 3759, 16 | "dislikes": 53 17 | } -------------------------------------------------------------------------------- /amplify/backend/analytics/youtubeclone/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "youtubeclone", 3 | "roleName": "pinpointLambdaRole4b90add6", 4 | "cloudformationPolicyName": "cloudformationPolicy4b90add6", 5 | "cloudWatchPolicyName": "cloudWatchPolicy4b90add6", 6 | "pinpointPolicyName": "pinpointPolicy4b90add6", 7 | "authPolicyName": "pinpoint_amplify_4b90add6", 8 | "unauthPolicyName": "pinpoint_amplify_4b90add6", 9 | "authRoleName": { 10 | "Ref": "AuthRoleName" 11 | }, 12 | "unauthRoleName": { 13 | "Ref": "UnauthRoleName" 14 | }, 15 | "authRoleArn": { 16 | "Fn::GetAtt": [ 17 | "AuthRole", 18 | "Arn" 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about deep linking with React Navigation 3 | * https://reactnavigation.org/docs/deep-linking 4 | * https://reactnavigation.org/docs/configuring-links 5 | */ 6 | 7 | import * as Linking from 'expo-linking'; 8 | 9 | export default { 10 | prefixes: [Linking.makeUrl('/')], 11 | config: { 12 | screens: { 13 | Root: { 14 | screens: { 15 | TabOne: { 16 | screens: { 17 | TabOneScreen: 'one', 18 | }, 19 | }, 20 | TabTwo: { 21 | screens: { 22 | TabTwoScreen: 'two', 23 | }, 24 | }, 25 | }, 26 | }, 27 | NotFound: '*', 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { View, StyleSheet, FlatList } from "react-native"; 3 | 4 | import { DataStore } from "aws-amplify"; 5 | import { Video } from "../src/models"; 6 | 7 | import VideoListItem from "../components/VideoListItem"; 8 | 9 | const HomeScreen = () => { 10 | const [videos, setVideos] = useState([]); 11 | 12 | useEffect(() => { 13 | // fetch videos 14 | DataStore.query(Video).then(setVideos); 15 | }, []); 16 | 17 | return ( 18 | 19 | } 22 | /> 23 | 24 | ); 25 | }; 26 | 27 | const styles = StyleSheet.create({}); 28 | 29 | export default HomeScreen; 30 | -------------------------------------------------------------------------------- /components/VideoPlayer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { View, Text } from "react-native"; 3 | import { Video } from "expo-av"; 4 | 5 | interface VideoPlayerProps { 6 | videoURI: string; 7 | thumbnailURI?: string; 8 | } 9 | 10 | const VideoPlayer = (props: VideoPlayerProps) => { 11 | const { videoURI, thumbnailURI } = props; 12 | 13 | return ( 14 | 15 | 29 | ); 30 | }; 31 | 32 | export default VideoPlayer; 33 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "YoutubeClone", 4 | "slug": "YoutubeClone", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": [ 19 | "**/*" 20 | ], 21 | "ios": { 22 | "supportsTablet": true 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/images/adaptive-icon.png", 27 | "backgroundColor": "#ffffff" 28 | } 29 | }, 30 | "web": { 31 | "favicon": "./assets/images/favicon.png" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /screens/TabOneScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import EditScreenInfo from '../components/EditScreenInfo'; 5 | import { Text, View } from '../components/Themed'; 6 | 7 | export default function TabOneScreen() { 8 | return ( 9 | 10 | Tab One 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | alignItems: 'center', 21 | justifyContent: 'center', 22 | }, 23 | title: { 24 | fontSize: 20, 25 | fontWeight: 'bold', 26 | }, 27 | separator: { 28 | marginVertical: 30, 29 | height: 1, 30 | width: '80%', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "staging": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-youtubeclone-staging-142749-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::704219588443:role/amplify-youtubeclone-staging-142749-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::704219588443:role/amplify-youtubeclone-staging-142749-authRole", 7 | "Region": "eu-west-1", 8 | "DeploymentBucketName": "amplify-youtubeclone-staging-142749-deployment", 9 | "UnauthRoleName": "amplify-youtubeclone-staging-142749-unauthRole", 10 | "StackName": "amplify-youtubeclone-staging-142749", 11 | "StackId": "arn:aws:cloudformation:eu-west-1:704219588443:stack/amplify-youtubeclone-staging-142749/330bead0-cac1-11eb-8a53-02f1248090e1", 12 | "AmplifyAppId": "d31f54b0flnyf8", 13 | "AuthTriggerTemplateURL": "" 14 | }, 15 | "categories": { 16 | "auth": { 17 | "YoutubeClone": {} 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /screens/VideoScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | videoPlayer: { 5 | width: '100%', 6 | aspectRatio: 16/9, 7 | }, 8 | videoInfoContainer: { 9 | margin: 10, 10 | }, 11 | title: { 12 | color: 'white', 13 | fontSize: 18, 14 | fontWeight: "500", 15 | marginVertical: 10, 16 | }, 17 | tags: { 18 | color: '#0094e3', 19 | fontSize: 14, 20 | fontWeight: "500", 21 | }, 22 | subtitle: { 23 | color: 'grey', 24 | fontSize: 14, 25 | fontWeight: "500", 26 | }, 27 | 28 | // action list 29 | actionListContainer: { 30 | marginVertical: 10, 31 | }, 32 | actionListItem: { 33 | width: 70, 34 | height: 60, 35 | justifyContent: 'space-around', 36 | alignItems: 'center', 37 | }, 38 | actionText: { 39 | color: 'white', 40 | }, 41 | 42 | // user 43 | avatar: { 44 | width: 50, 45 | height: 50, 46 | borderRadius: 25, 47 | } 48 | }); 49 | 50 | export default styles; -------------------------------------------------------------------------------- /components/VideoComment/index.tsx: -------------------------------------------------------------------------------- 1 | import { DataStore } from "aws-amplify"; 2 | import React, { useState, useEffect } from "react"; 3 | import { View, Text, Image } from "react-native"; 4 | import { Comment, User } from "../../src/models"; 5 | 6 | interface VideoCommentProps { 7 | comment: Comment; 8 | } 9 | 10 | const VideoComment = ({ comment }: VideoCommentProps) => { 11 | const [user, setUser] = useState(null); 12 | 13 | useEffect(() => { 14 | DataStore.query(User, comment.userID as string).then(setUser); 15 | }); 16 | 17 | return ( 18 | 21 | 25 | 26 | {user?.name} 27 | 28 | {comment.comment} 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default VideoComment; 36 | -------------------------------------------------------------------------------- /components/VideoListItem/styles.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | videoCard: { 5 | marginVertical: 15 6 | }, 7 | thumbnail: { 8 | width: '100%', 9 | aspectRatio: 16/9, 10 | }, 11 | timeContainer: { 12 | backgroundColor: '#00000099', 13 | height: 25, 14 | width: 50, 15 | justifyContent: 'center', 16 | alignItems: 'center', 17 | borderRadius: 4, 18 | position: 'absolute', 19 | right: 5, 20 | bottom: 5, 21 | }, 22 | time: { 23 | color: 'white', 24 | fontWeight: 'bold', 25 | }, 26 | avatar: { 27 | width: 50, 28 | height: 50, 29 | borderRadius: 25, 30 | }, 31 | titleRow: { 32 | flexDirection: 'row', 33 | padding: 10, 34 | }, 35 | midleContainer: { 36 | marginHorizontal: 10, 37 | flex: 1, 38 | }, 39 | title: { 40 | color: 'white', 41 | fontSize: 18, 42 | fontWeight: "500", 43 | marginBottom: 5, 44 | }, 45 | subtitle: { 46 | color: 'grey', 47 | fontSize: 14, 48 | fontWeight: "500", 49 | } 50 | }); 51 | 52 | export default styles; -------------------------------------------------------------------------------- /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/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "YoutubeClone": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [], 7 | "customAuth": false 8 | } 9 | }, 10 | "api": { 11 | "YoutubeClone": { 12 | "service": "AppSync", 13 | "providerPlugin": "awscloudformation", 14 | "output": { 15 | "authConfig": { 16 | "defaultAuthentication": { 17 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 18 | "userPoolConfig": { 19 | "userPoolId": "authYoutubeClone" 20 | } 21 | }, 22 | "additionalAuthenticationProviders": [ 23 | { 24 | "authenticationType": "AWS_IAM" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | }, 31 | "storage": { 32 | "youtubeclonemedia": { 33 | "service": "S3", 34 | "providerPlugin": "awscloudformation" 35 | } 36 | }, 37 | "analytics": { 38 | "youtubeclone": { 39 | "service": "Pinpoint", 40 | "providerPlugin": "awscloudformation" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "validatetypenamereservedwords": true, 6 | "useexperimentalpipelinedtransformer": false, 7 | "enableiterativegsiupdates": true, 8 | "secondarykeyasgsi": true, 9 | "skipoverridemutationinputtypes": true 10 | }, 11 | "frontend-ios": { 12 | "enablexcodeintegration": true 13 | }, 14 | "auth": { 15 | "enablecaseinsensitivity": true, 16 | "useinclusiveterminology": true, 17 | "breakcirculardependency": true 18 | }, 19 | "codegen": { 20 | "useappsyncmodelgenplugin": true, 21 | "usedocsgeneratorplugin": true, 22 | "usetypesgeneratorplugin": true, 23 | "cleangeneratedmodelsdirectory": true, 24 | "retaincasestyle": true, 25 | "addtimestampfields": true, 26 | "handlelistnullabilitytransparently": true 27 | }, 28 | "appsync": { 29 | "generategraphqlpermissions": true 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /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 { StyleSheet, Pressable } from "react-native"; 3 | 4 | import { Auth } from "aws-amplify"; 5 | 6 | import EditScreenInfo from "../components/EditScreenInfo"; 7 | import { Text, View } from "../components/Themed"; 8 | 9 | export default function TabTwoScreen() { 10 | const signOut = () => { 11 | Auth.signOut(); 12 | }; 13 | 14 | return ( 15 | 16 | Tab Two 17 | 22 | 23 | 24 | 25 | Sign out 26 | 27 | 28 | ); 29 | } 30 | 31 | const styles = StyleSheet.create({ 32 | container: { 33 | flex: 1, 34 | alignItems: "center", 35 | justifyContent: "center", 36 | }, 37 | title: { 38 | fontSize: 20, 39 | fontWeight: "bold", 40 | }, 41 | separator: { 42 | marginVertical: 30, 43 | height: 1, 44 | width: "80%", 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /amplify/backend/api/YoutubeClone/schema.graphql: -------------------------------------------------------------------------------- 1 | type Comment @model @key(name: "byVideo", fields: ["videoID"]) @auth(rules: [{allow: owner, operations: [read, create, update, delete]}, {allow: private, operations: [read, create]}]) @key(name: "byUser", fields: ["userID"]) { 2 | id: ID! 3 | comment: String! 4 | likes: Int! 5 | dislikes: Int! 6 | replies: Int! 7 | videoID: ID 8 | User: User @connection 9 | Video: Video @connection 10 | userID: ID 11 | } 12 | 13 | type User @model @auth(rules: [{allow: private, operations: [read, create, update, delete]}, {allow: owner, operations: [read, create]}]) { 14 | id: ID! 15 | name: String! 16 | image: String 17 | subscribers: Int 18 | Videos: [Video] @connection(keyName: "byUser", fields: ["id"]) 19 | Comments: [Comment] @connection(keyName: "byUser", fields: ["id"]) 20 | sub: String 21 | } 22 | 23 | type Video @model @key(name: "byUser", fields: ["userID"]) @auth(rules: [{allow: owner, operations: [read, create, update, delete]}, {allow: private, operations: [read, create]}]) { 24 | id: ID! 25 | title: String! 26 | thumbnail: String! 27 | videoUrl: String! 28 | duration: Float! 29 | views: Int! 30 | tags: String 31 | likes: Int! 32 | dislikes: String! 33 | User: User @connection 34 | Comments: [Comment] @connection(keyName: "byVideo", fields: ["id"]) 35 | userID: ID 36 | } 37 | -------------------------------------------------------------------------------- /amplify/backend/storage/youtubeclonemedia/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucketName": "youtubeclonemedia", 3 | "authPolicyName": "s3_amplify_857fee66", 4 | "unauthPolicyName": "s3_amplify_857fee66", 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:PutObject", 17 | "s3:GetObject", 18 | "s3:ListBucket", 19 | "s3:DeleteObject" 20 | ], 21 | "s3PermissionsAuthenticatedPublic": "s3:PutObject,s3:GetObject,s3:DeleteObject", 22 | "s3PublicPolicy": "Public_policy_dfdaf5de", 23 | "s3PermissionsAuthenticatedUploads": "s3:PutObject", 24 | "s3UploadsPolicy": "Uploads_policy_dfdaf5de", 25 | "s3PermissionsAuthenticatedProtected": "s3:PutObject,s3:GetObject,s3:DeleteObject", 26 | "s3ProtectedPolicy": "Protected_policy_4f8abc83", 27 | "s3PermissionsAuthenticatedPrivate": "s3:PutObject,s3:GetObject,s3:DeleteObject", 28 | "s3PrivatePolicy": "Private_policy_4f8abc83", 29 | "AuthenticatedAllowList": "ALLOW", 30 | "s3ReadPolicy": "read_policy_dfdaf5de", 31 | "s3PermissionsGuestPublic": "s3:GetObject", 32 | "s3PermissionsGuestUploads": "DISALLOW", 33 | "GuestAllowList": "ALLOW", 34 | "triggerFunction": "NONE" 35 | } -------------------------------------------------------------------------------- /components/Themed.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about Light and Dark modes: 3 | * https://docs.expo.io/guides/color-schemes/ 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { Text as DefaultText, View as DefaultView } from 'react-native'; 8 | 9 | import Colors from '../constants/Colors'; 10 | import useColorScheme from '../hooks/useColorScheme'; 11 | 12 | export function useThemeColor( 13 | props: { light?: string; dark?: string }, 14 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 15 | ) { 16 | const theme = useColorScheme(); 17 | const colorFromProps = props[theme]; 18 | 19 | if (colorFromProps) { 20 | return colorFromProps; 21 | } else { 22 | return Colors[theme][colorName]; 23 | } 24 | } 25 | 26 | type ThemeProps = { 27 | lightColor?: string; 28 | darkColor?: string; 29 | }; 30 | 31 | export type TextProps = ThemeProps & DefaultText['props']; 32 | export type ViewProps = ThemeProps & DefaultView['props']; 33 | 34 | export function Text(props: TextProps) { 35 | const { style, lightColor, darkColor, ...otherProps } = props; 36 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 37 | 38 | return ; 39 | } 40 | 41 | export function View(props: ViewProps) { 42 | const { style, lightColor, darkColor, ...otherProps } = props; 43 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 44 | 45 | return ; 46 | } 47 | -------------------------------------------------------------------------------- /navigation/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * If you are not familiar with React Navigation, check out the "Fundamentals" guide: 3 | * https://reactnavigation.org/docs/getting-started 4 | * 5 | */ 6 | import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; 7 | import { createStackNavigator } from '@react-navigation/stack'; 8 | import * as React from 'react'; 9 | import { ColorSchemeName } from 'react-native'; 10 | 11 | import NotFoundScreen from '../screens/NotFoundScreen'; 12 | import VideoScreen from '../screens/VideoScreen'; 13 | import { RootStackParamList } from '../types'; 14 | import BottomTabNavigator from './BottomTabNavigator'; 15 | import LinkingConfiguration from './LinkingConfiguration'; 16 | 17 | export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) { 18 | return ( 19 | 22 | 23 | 24 | ); 25 | } 26 | 27 | // A root stack navigator is often used for displaying modals on top of all other content 28 | // Read more here: https://reactnavigation.org/docs/modal 29 | const Stack = createStackNavigator(); 30 | 31 | function RootNavigator() { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /amplify/backend/api/YoutubeClone/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 | -------------------------------------------------------------------------------- /assets/data/videos.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "1", 3 | "createdAt": "5 months ago", 4 | "title": "Build a Realtime Chat App in React Native (tutorial for beginners) 🔴 ", 5 | "thumbnail": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/thumbnails/thumbnail1.jpeg", 6 | "videoUrl": "", 7 | "duration": 66, 8 | "user": { 9 | "name": "Vadim Savin", 10 | "image": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg" 11 | }, 12 | "views": 32345123 13 | }, { 14 | "id": "2", 15 | "createdAt": "2 months ago", 16 | "title": "🔴 Build the Uber clone in React Native (Tutorial for Beginners)", 17 | "thumbnail": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/thumbnails/thumbnail2.jpeg", 18 | "videoUrl": "", 19 | "duration": 584, 20 | "user": { 21 | "name": "Vadim Savin", 22 | "image": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg" 23 | }, 24 | "views": 257000 25 | }, { 26 | "id": "3", 27 | "createdAt": "2 months ago", 28 | "title": "How Graham Stephan makes $4,578,896.32 a year on YouTube", 29 | "thumbnail": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/thumbnails/thumbnail3.jpeg", 30 | "videoUrl": "", 31 | "duration": 584, 32 | "user": { 33 | "name": "Graham Stephan", 34 | "image": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/graham.jpg" 35 | }, 36 | "views": 257000 37 | }, { 38 | "id": "4", 39 | "createdAt": "2 months ago", 40 | "title": "My One Month Bitcoin Mining Journey", 41 | "thumbnail": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/thumbnails/thumbnail4.jpeg", 42 | "videoUrl": "", 43 | "duration": 584, 44 | "user": { 45 | "name": "Biahaze", 46 | "image": "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/biahaze.jpg" 47 | }, 48 | "views": 257000 49 | }] -------------------------------------------------------------------------------- /src/models/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ModelInit, MutableModel, PersistentModelConstructor } from "@aws-amplify/datastore"; 2 | 3 | 4 | 5 | 6 | 7 | export declare class Comment { 8 | readonly id: string; 9 | readonly comment: string; 10 | readonly likes: number; 11 | readonly dislikes: number; 12 | readonly replies: number; 13 | readonly videoID?: string; 14 | readonly User?: User; 15 | readonly Video?: Video; 16 | readonly userID?: string; 17 | readonly createdAt?: string; 18 | readonly updatedAt?: string; 19 | constructor(init: ModelInit); 20 | static copyOf(source: Comment, mutator: (draft: MutableModel) => MutableModel | void): Comment; 21 | } 22 | 23 | export declare class User { 24 | readonly id: string; 25 | readonly name: string; 26 | readonly image?: string; 27 | readonly subscribers?: number; 28 | readonly Videos?: (Video | null)[]; 29 | readonly Comments?: (Comment | null)[]; 30 | readonly sub?: string; 31 | readonly createdAt?: string; 32 | readonly updatedAt?: string; 33 | constructor(init: ModelInit); 34 | static copyOf(source: User, mutator: (draft: MutableModel) => MutableModel | void): User; 35 | } 36 | 37 | export declare class Video { 38 | readonly id: string; 39 | readonly title: string; 40 | readonly thumbnail: string; 41 | readonly videoUrl: string; 42 | readonly duration: number; 43 | readonly views: number; 44 | readonly tags?: string; 45 | readonly likes: number; 46 | readonly dislikes: string; 47 | readonly User?: User; 48 | readonly Comments?: (Comment | null)[]; 49 | readonly userID?: string; 50 | readonly createdAt?: string; 51 | readonly updatedAt?: string; 52 | constructor(init: ModelInit