├── .gitignore ├── exercise ├── .vscode │ └── settings.json ├── index.html ├── js │ └── index.js └── style │ └── style.css ├── readme.md └── solution ├── .vscode └── settings.json ├── assets └── profile.png ├── index.html ├── js └── index.js └── style ├── style.css └── tweet.css /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # IDEs and editors 13 | .idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # Logs 29 | logs 30 | *.log 31 | 32 | 33 | # Dependency directories 34 | node_modules/ 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # dotenv environment variables file 40 | .env 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db -------------------------------------------------------------------------------- /exercise/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /exercise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Title 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /exercise/js/index.js: -------------------------------------------------------------------------------- 1 | const URL = "http://localhost:3000/tweets"; 2 | 3 | /** 4 | * Retrive Twitter Data from API 5 | */ 6 | const getTwitterData = () => { 7 | 8 | } 9 | 10 | /** 11 | * Save the next page data 12 | */ 13 | const saveNextPage = (metadata) => { 14 | } 15 | 16 | /** 17 | * Handle when a user clicks on a trend 18 | */ 19 | const selectTrend = (e) => { 20 | } 21 | 22 | /** 23 | * Set the visibility of next page based on if there is data on next page 24 | */ 25 | const nextPageButtonVisibility = (metadata) => { 26 | } 27 | 28 | /** 29 | * Build Tweets HTML based on Data from API 30 | */ 31 | const buildTweets = (tweets, nextPage) => { 32 | 33 | } 34 | 35 | /** 36 | * Build HTML for Tweets Images 37 | */ 38 | const buildImages = (mediaList) => { 39 | 40 | } 41 | 42 | /** 43 | * Build HTML for Tweets Video 44 | */ 45 | const buildVideo = (mediaList) => { 46 | 47 | } 48 | -------------------------------------------------------------------------------- /exercise/style/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Arial, Helvetica, sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # General TODOS 2 | 3 | ## TODO(Together): Create the home page structure for index.html 4 | 5 | That involves the navigation, tweets list, and trending hashtags section. 6 | 7 | ## TODO: Create HTML and Style for Navigation 8 | 9 | Use Mockup for styles like border 10 | 11 | It needs to follow a class structure like this: 12 | 13 | class navigation 14 | 15 | class logo 16 | 17 | 18 | 19 | class home-link 20 | 21 | 22 | 23 | class profile-container 24 | 25 | class profile 26 | 27 | ## TODO(Together): Create HTML and Style for Input Box 28 | 29 | ## TODO(Together): Create HTML and Style for Individual Tweet 30 | 31 | ### TODO: Complete User Info HTML and Styling 32 | 33 | It needs to follow a class structure like this: 34 | 35 | class tweet-user-info 36 | 37 | class tweet-user-profile 38 | 39 | class tweet-user-name-container 40 | 41 | class tweet-user-fullname 42 | 43 | class tweet-user-username 44 | 45 | #### HINTS: 46 | 47 | User Profile: width: 30px; height: 30px; 48 | 49 | User Full Name: font-size: 10px 50 | 51 | User Twitter Handel: font-size: 8px 52 | 53 | ## TODO(Together): Create HTML and Style for Trending Box 54 | 55 | ### TODO: Complete styling for list of trends 56 | 57 | #### HINTS: 58 | 59 | List Item: padding-left: 20px; padding-top and bottom: 8px 60 | 61 | ## TODO(API): Set up NodeJS Server 62 | 63 | Return `Hello World` for the root `/` get request 64 | 65 | [Example](https://expressjs.com/en/starter/hello-world.html) 66 | 67 | ## TODO(API, TOGETHER): Create API endpoint `/tweeets` to return a list of tweets based on query 68 | 69 | Use [axios](https://github.com/axios/axios) for making an API request to Twitter API 70 | 71 | Console log the data 72 | 73 | Return as a response 74 | 75 | ## TODO(API): Create Twitter `get()` helper function to move the Twitter API logic 76 | 77 | #### HINTS: 78 | 79 | - Create Twitter class inside `api/helpers/twitter.js` 80 | - Create a `get()` function that takes in the necessary parameters 81 | - Inside `get()` return `axios.get(...)` 82 | - Import Twitter class in `app.js` with `const twitter = new Twitter();` 83 | - Initialize and use the `twitter` object to now do somethong like `twitter.get(...).then(...).catch(...)` 84 | 85 | ## TODO(API, TOGETHER): Move the API Token to .env file and import it 86 | 87 | ## TODO: Complete `getTwitterData()` function to retrieve data from our API 88 | 89 | For now, I want you to use the following static url to get data from api: 90 | 91 | ```http://localhost:3000/tweets?q=coding&count=10``` 92 | 93 | #### HINTS: 94 | 95 | - Use `fetch()` 96 | - Call function on load of js 97 | - Console log response 98 | 99 | ## TODO: Get search input and use it to build a `url` like the one above 100 | 101 | This time you are building a dynamic url that will change based on the user's search input 102 | 103 | #### HINTS: 104 | 105 | - Call `getTwitterData()` function when a user clicks on search icon 106 | - Use string literals to build out the url 107 | - Console log response 108 | 109 | ### TODO(Together): Get twitter data when a user hits enter 110 | 111 | 112 | ## TODO: Complete `buildTweets()` function to show the Tweets List(only text) 113 | 114 | #### HINTS: 115 | 116 | - Call `buildTweets()` function from `getTwitterData()` 117 | - Use List.map() to loop over the list of tweets 118 | - Use string literals to replace html with the text from each tweet 119 | - Replace html content inside `.tweets-list` 120 | 121 | ## TODO: Add abiliy to show images in the tweets 122 | 123 | #### HNTS: 124 | 125 | - Use `buildImages()` function 126 | - Check if there is media using `.length`property to call `buildImages()` function 127 | 128 | ## TODO(Together): Add ability to show videos in the tweets 129 | 130 | ## TODO(Together): Add ability to show gifs in the tweets 131 | 132 | ## TODO: Show user info in the tweets 133 | 134 | ## TODO(Together): Use [moment.js](https://momentjs.com/) to show the date of tweet 135 | 136 | ## TODO: Complete `selectTrend()` function to allow a user to click on the trend and search for it 137 | 138 | #### HINTS: 139 | 140 | - Call `selectTrend()` function from list item click 141 | - Get the inner text of list item 142 | - Set the value of input search using the text 143 | - Call `getTwitterData()` function 144 | 145 | ## TODO: Create HTML and Style for Next Page Button 146 | 147 | #### HINTS: 148 | 149 | - Use `next-page-container` class 150 | - border-radius: 20px; margin-top: 20px; 151 | - Use arrow down font awesome icon 152 | 153 | ## TODO(Together): Showing Next Page of Tweets 154 | 155 | ### TODO: Save Next Page Url 156 | 157 | ### TODO: Load tweets when selecting the next page button 158 | 159 | [Here](https://developer.twitter.com/en/docs/tweets/timelines/guides/working-with-timelines) is how twitter pagination works 160 | 161 | ### TODO(API): ...something that has to do with `max_id` 162 | 163 | ### TODO: Fix logic to replace tweets when searching, but append tweets when going to next page 164 | 165 | ### TODO: Show next page button only when there is a next page 166 | 167 | ## TODO: Clean Up 168 | 169 | ## WE ARE DONE! 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /solution/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /solution/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleverProgrammers/pwj-twitter-clone-app/917497a748e2b21333d4aceae37e48653ede1278/solution/assets/profile.png -------------------------------------------------------------------------------- /solution/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Twitter Tweet 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 |
Welcome to Twitter!
42 |

Use the search above to see what's happening around the world.

43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /solution/js/index.js: -------------------------------------------------------------------------------- 1 | const URL = "http://localhost:3000/tweets"; 2 | const nextPageData = { 3 | loading: false, 4 | url: null 5 | } 6 | 7 | const onEnter = (e) => { 8 | if(e.key == "Enter") { 9 | getTwitterData(); 10 | } 11 | } 12 | 13 | const onNextPage = () => { 14 | if(nextPageData.url){ 15 | getTwitterData(true); 16 | } 17 | } 18 | 19 | const getTwitterData = (nextPage=false) => { 20 | const query = document.getElementById("user-search-input").value; 21 | if(!query) return; 22 | const encodedQuery = encodeURIComponent(query); 23 | const params = `q=${encodedQuery}&result_type=mixed`; 24 | let fullUrl = `${URL}?${params}`; 25 | if(nextPage){ 26 | fullUrl = nextPageData.url; 27 | nextPageData.loading = true; 28 | } 29 | fetch(fullUrl, { 30 | method: 'GET' 31 | }).then((response)=>{ 32 | return response.json(); 33 | }).then((data)=>{ 34 | saveNextPage(data.search_metadata) 35 | buildTweets(data.statuses, nextPage); 36 | nextPageButtonVisibility(data.search_metadata); 37 | }); 38 | } 39 | 40 | const saveNextPage = (metadata) => { 41 | nextPageData.url = `${URL}${metadata.next_results}` 42 | nextPageData.loading = false; 43 | } 44 | 45 | const selectTrend = (e) => { 46 | const trendText = e.innerText; 47 | document.getElementById("user-search-input").value = trendText; 48 | getTwitterData(); 49 | } 50 | 51 | const nextPageButtonVisibility = (metadata) => { 52 | let visibility = 'hidden'; 53 | if(metadata.next_results){ 54 | visibility = 'visible'; 55 | } 56 | document.getElementById('next-page').style.visibility = visibility; 57 | } 58 | 59 | const buildTweets = (tweets, nextPage) => { 60 | let twitterContent = ""; 61 | tweets.map((tweet)=>{ 62 | const createdDate = moment(tweet.created_at).fromNow(); 63 | twitterContent += ` 64 |
65 |
66 | 67 |
68 |
${tweet.user.name}
69 |
@${tweet.user.screen_name}
70 |
71 |
72 | ` 73 | if(tweet.extended_entities 74 | && tweet.extended_entities.media 75 | && tweet.extended_entities.media.length > 0){ 76 | twitterContent += buildImages(tweet.extended_entities.media); 77 | twitterContent += buildVideo(tweet.extended_entities.media); 78 | } 79 | twitterContent += ` 80 |
81 | 82 | ${tweet.full_text} 83 | 84 |
85 |
86 | ${createdDate} 87 |
88 |
89 | ` 90 | }) 91 | if(nextPage){ 92 | document.querySelector('.tweets-list').insertAdjacentHTML('beforeend', twitterContent) 93 | } else { 94 | document.querySelector('.tweets-list').innerHTML = twitterContent; 95 | } 96 | } 97 | 98 | const buildImages = (mediaList) => { 99 | let imagesContent = `
`; 100 | let imagesExist = false; 101 | mediaList.map((media)=>{ 102 | if(media.type == "photo"){ 103 | imagesExist = true; 104 | imagesContent += ` 105 |
106 | ` 107 | } 108 | }) 109 | imagesContent += `
`; 110 | return (imagesExist ? imagesContent : ''); 111 | } 112 | 113 | const buildVideo = (mediaList) => { 114 | let videoContent = `
`; 115 | let videoExists = false; 116 | mediaList.map((media)=>{ 117 | if(media.type == "video" || media.type == 'animated_gif'){ 118 | videoExists = true; 119 | const video = media.video_info.variants.find((video)=>video.content_type == 'video/mp4'); 120 | const videoOptions = getVideoOptions(media.type); 121 | videoContent += ` 122 | 126 | ` 127 | } 128 | }) 129 | videoContent += `
`; 130 | return (videoExists ? videoContent : ''); 131 | } 132 | 133 | const getVideoOptions = (mediaType) => { 134 | if(mediaType == 'animated_gif'){ 135 | return "loop autoplay"; 136 | } else { 137 | return "controls"; 138 | } 139 | } -------------------------------------------------------------------------------- /solution/style/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Arial, Helvetica, sans-serif; 4 | background-color: #0F0F11; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | min-height: 100vh; 10 | } 11 | 12 | .navigation-container { 13 | border-right: 1px solid #1C1C1C; 14 | max-width: 100px; 15 | min-width: 70px; 16 | width: 20%; 17 | } 18 | 19 | .navigation { 20 | position: fixed; 21 | top: 0; 22 | left: 0; 23 | max-width: 100px; 24 | min-width: 70px; 25 | width: 20%; 26 | } 27 | 28 | nav { 29 | padding-top: 40px; 30 | } 31 | 32 | .logo { 33 | display: flex; 34 | justify-content: center; 35 | font-size: 40px; 36 | color: white; 37 | } 38 | 39 | .home-link { 40 | color: #00ADFF; 41 | display: flex; 42 | justify-content: center; 43 | margin-top: 20px; 44 | position: relative; 45 | height: 40px; 46 | align-items: center; 47 | } 48 | 49 | .home-link::after { 50 | content: ""; 51 | position: absolute; 52 | top: 0; 53 | right: 0; 54 | bottom: 0; 55 | width: 5px; 56 | background-color: #00ADFF; 57 | } 58 | 59 | .profile-container { 60 | display: flex; 61 | width: 100%; 62 | margin-top: 20px; 63 | justify-content: center; 64 | } 65 | 66 | .profile { 67 | width: 30px; 68 | height: 30px; 69 | background-color: white; 70 | border-radius: 50%; 71 | border: 1px solid #979797; 72 | background-position: center; 73 | background-size: cover; 74 | background-image: url(/assets/profile.png); 75 | } 76 | 77 | .main { 78 | flex: 1; 79 | padding-top: 40px; 80 | display: flex; 81 | overflow: scroll; 82 | } 83 | 84 | .tweets-container { 85 | flex: 1; 86 | padding-left: 40px; 87 | padding-right: 40px; 88 | padding-bottom: 40px; 89 | } 90 | 91 | .tweets-sidebar-container { 92 | flex: 1; 93 | position: relative; 94 | } 95 | 96 | .tweets-sidebar { 97 | width: 300px; 98 | height: 500px; 99 | background-color: white; 100 | border-radius: 40px; 101 | position: fixed; 102 | border: 1px solid #979797; 103 | } 104 | 105 | .tweets-trending { 106 | background-color: #F2F2F2; 107 | border-radius: 40px; 108 | margin: 10px; 109 | display: flex; 110 | flex-direction: column; 111 | padding-bottom: 20px; 112 | overflow: hidden; 113 | } 114 | 115 | .tweets-trending h5 { 116 | font-size: 12px; 117 | color: #848586; 118 | margin-top: 30px; 119 | margin-bottom: 15px; 120 | padding-left: 20px; 121 | } 122 | 123 | .tweets-trending ul { 124 | list-style-type: none; 125 | list-style: none; 126 | padding: 0; 127 | margin: 0; 128 | } 129 | 130 | .tweets-trending ul li { 131 | display: block; 132 | padding-top: 8px; 133 | padding-bottom: 8px; 134 | border-top: 0.5px solid #D8D8D8; 135 | font-weight: 600; 136 | padding-left: 20px; 137 | color: #A5A5A6; 138 | cursor: pointer; 139 | } 140 | 141 | .tweets-search-input { 142 | padding-left: 10px; 143 | padding-right: 10px; 144 | display: flex; 145 | align-items: center; 146 | border-bottom: 1px solid #979797; 147 | } 148 | 149 | .tweets-search-input input { 150 | flex: 1; 151 | background-color: transparent; 152 | border: none; 153 | color: white; 154 | font-size: 30px; 155 | padding-top: 3px; 156 | padding-bottom: 3px; 157 | } 158 | 159 | .tweets-search-input input:focus { 160 | outline: none; 161 | } 162 | 163 | .tweets-search-input i { 164 | font-size: 30px; 165 | color: white; 166 | cursor: pointer; 167 | } -------------------------------------------------------------------------------- /solution/style/tweet.css: -------------------------------------------------------------------------------- 1 | .tweet-container { 2 | margin-top: 20px; 3 | border-radius: 20px; 4 | background-position: center; 5 | background-size: cover; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | border: 1px solid #1C1C1C; 10 | } 11 | 12 | .tweet-text-container { 13 | color: #ffffffbf; 14 | font-size: 15px; 15 | overflow-y: hidden; 16 | margin-left: 20px; 17 | margin-right: 20px; 18 | display: flex; 19 | margin-top: 0; 20 | margin-bottom: 10px; 21 | } 22 | 23 | .tweet-user-info { 24 | margin-left: 20px; 25 | margin-top: 10px; 26 | margin-bottom: 20px; 27 | display: flex; 28 | align-items: center; 29 | } 30 | 31 | .tweet-user-profile { 32 | width: 30px; 33 | height: 30px; 34 | background-color: white; 35 | border-radius: 50%; 36 | border: 1px solid #979797; 37 | background-position: center; 38 | background-size: cover; 39 | } 40 | 41 | .tweet-user-name-container { 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: center; 45 | margin-left: 5px; 46 | padding: 6px; 47 | border-radius: 5px; 48 | } 49 | 50 | .tweet-user-fullname { 51 | color: white; 52 | font-size: 10px; 53 | } 54 | 55 | .tweet-user-username { 56 | color: white; 57 | font-size: 8px; 58 | } 59 | 60 | .tweet-text { 61 | overflow: hidden; 62 | } 63 | 64 | .tweet-images-container { 65 | border: 1px solid #1C1C1C; 66 | border-radius: 20px; 67 | height: 230px; 68 | overflow: hidden; 69 | margin-left: 20px; 70 | margin-bottom: 20px; 71 | margin-right: 20px; 72 | display: flex; 73 | flex-wrap: wrap; 74 | } 75 | 76 | .tweet-image { 77 | background-position: center; 78 | background-size: contain; 79 | background-repeat: no-repeat; 80 | flex-basis: 50%; 81 | flex-grow: 1; 82 | } 83 | 84 | .tweet-video-container { 85 | position: relative; 86 | border: 1px solid #1C1C1C; 87 | border-radius: 20px; 88 | height: 230px; 89 | overflow: hidden; 90 | margin-left: 20px; 91 | margin-bottom: 20px; 92 | margin-right: 20px; 93 | } 94 | 95 | .tweet-video-container video { 96 | width: 100%; 97 | height: 100%; 98 | position: absolute; 99 | top: 0%; 100 | left: 0%; 101 | } 102 | 103 | .tweets-welcome-message { 104 | color: white; 105 | text-align: center; 106 | max-width: 80%; 107 | margin: 0 auto; 108 | } 109 | 110 | .tweets-welcome-message h5 { 111 | margin-bottom: 0; 112 | } 113 | 114 | .tweets-welcome-message p { 115 | font-size: 10px; 116 | } 117 | 118 | #next-page { 119 | margin-top: 20px; 120 | border-radius: 20px; 121 | display: flex; 122 | visibility: hidden; 123 | border: 1px solid #1C1C1C; 124 | justify-content: center; 125 | align-items: center; 126 | padding: 6px; 127 | color: #47525d; 128 | cursor: pointer; 129 | transition: all 0.2s ease-in-out; 130 | } 131 | 132 | #next-page:hover { 133 | background-color: #1C1C1C; 134 | } 135 | 136 | .tweet-date { 137 | color: #47525d; 138 | font-size: 10px; 139 | margin-left: 20px; 140 | margin-bottom: 10px; 141 | } --------------------------------------------------------------------------------