├── README.md ├── interfaces.ts ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── sample-data.json ├── server.ts ├── src ├── App.tsx ├── components │ ├── AnswerBlock.tsx │ ├── QuestionBlock.tsx │ ├── QuestionsBlock.tsx │ └── Title.tsx ├── index.css └── index.tsx └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # Build a Buzzfeed Clone in TypeScript + REST API Database + Node.js! 2 | 3 | Watch the full tutorial here: Build a Buzzfeed Clone in TypeScript + REST API Database + Node.js! 4 | -------------------------------------------------------------------------------- /interfaces.ts: -------------------------------------------------------------------------------- 1 | interface QuizData { 2 | title: string; 3 | subtitle: string; 4 | quizId: string; 5 | content: Content[]; 6 | answers: Answer[]; 7 | } 8 | 9 | interface Answer { 10 | text: string; 11 | image: string; 12 | alt: string; 13 | combination: string[] 14 | } 15 | 16 | interface Content { 17 | id: number; 18 | text: string; 19 | questions: Question[]; 20 | } 21 | 22 | interface Question { 23 | text: string; 24 | image: string; 25 | alt: string; 26 | credit: string; 27 | } 28 | 29 | export type { QuizData, Answer, Content, Question} 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-buzzfeed-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/express": "^4.17.13", 7 | "@types/node": "^16.11.58", 8 | "@types/react": "^18.0.19", 9 | "@types/react-dom": "^18.0.6", 10 | "axios": "^0.27.2", 11 | "dotenv": "^16.0.2", 12 | "nodemon": "^2.0.19", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-scripts": "5.0.1", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^4.8.3", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start:frontend": "react-scripts start", 22 | "start:backend": "nodemon server.ts", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubowania/buzzfeed-clone-typescript-database/d802c44f96117a2e0cca2c69702acb5eccabccf4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubowania/buzzfeed-clone-typescript-database/d802c44f96117a2e0cca2c69702acb5eccabccf4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubowania/buzzfeed-clone-typescript-database/d802c44f96117a2e0cca2c69702acb5eccabccf4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /sample-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "quizId": "3483j33", 3 | "title": "What cheese are you?", 4 | "subtitle": "This quiz isn't cheesy or anything like that...", 5 | "content": [ 6 | { 7 | "id": 0, 8 | "text": "Pick a vacation destination:", 9 | "questions": [ 10 | { 11 | "text": "New York", 12 | "image": "https://images.unsplash.com/photo-1534430480872-3498386e7856?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 13 | "alt": "Photo of Empire State Building during daytime", 14 | "credit": "Oliver Niblett" 15 | }, 16 | { 17 | "text": "Austin", 18 | "image": "https://images.unsplash.com/photo-1531218150217-54595bc2b934?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 19 | "alt": "Time-lapse photography car lights on bridge", 20 | "credit": "Carlos Alfonso" 21 | }, 22 | { 23 | "text": "Portland", 24 | "image": "https://images.unsplash.com/photo-1534430480872-3498386e7856?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 25 | "alt": "High-rise buildings", 26 | "credit": "Elena Kuchko" 27 | }, 28 | { 29 | "text": "New Orleans", 30 | "image": "https://images.unsplash.com/photo-1549965738-e1aaf1168943?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 31 | "alt": "Road with people and house", 32 | "credit": "João Francisco" 33 | } 34 | ] 35 | }, 36 | { 37 | "id": 1, 38 | "text": "Pick some food:", 39 | "questions": [ 40 | { 41 | "text": "Pizza", 42 | "image": "https://images.unsplash.com/photo-1534308983496-4fabb1a015ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 43 | "alt": "Pepperoni Pizza", 44 | "credit": "Alan Hardman" 45 | }, 46 | { 47 | "text": "Sandwich", 48 | "image": "https://images.unsplash.com/photo-1481070414801-51fd732d7184?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 49 | "alt": "ham sandwich on white surface", 50 | "credit": "Eaters Collective" 51 | }, 52 | { 53 | "text": "Pasta", 54 | "image": "https://images.unsplash.com/photo-1516100882582-96c3a05fe590?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 55 | "alt": "Pasta in tomato sauce", 56 | "credit": "Mgg Vitchakorn" 57 | }, 58 | { 59 | "text": "Hamburger", 60 | "image": "https://images.unsplash.com/photo-1551782450-a2132b4ba21d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 61 | "alt": "hamburger", 62 | "credit": "sk" 63 | } 64 | ] 65 | }, 66 | { 67 | "id": 2, 68 | "text": "Pick a home:", 69 | "questions": [ 70 | { 71 | "text": "Traditional", 72 | "image": "https://images.unsplash.com/photo-1555040479-c949debe66c1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 73 | "alt": "focus photography of building windows", 74 | "credit": "Burgess Milner" 75 | }, 76 | { 77 | "text": "Modern", 78 | "image": "https://images.unsplash.com/photo-1460317442991-0ec209397118?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 79 | "alt": "low angle view of building", 80 | "credit": "Brandon Giggs" 81 | }, 82 | { 83 | "text": "House", 84 | "image": "https://images.unsplash.com/photo-1572120360610-d971b9d7767c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 85 | "alt": "trees beside white house", 86 | "credit": "Phil Hearing" 87 | }, 88 | { 89 | "text": "Mountains", 90 | "image": "https://images.unsplash.com/photo-1506974210756-8e1b8985d348?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&fit=crop&h=230&w=320&crop=edges", 91 | "alt": "brown wooden cabin infront of forest", 92 | "credit": "eulauretta" 93 | } 94 | ] 95 | } 96 | ], 97 | "answers": [ 98 | { 99 | "combination": ["New York", "Pizza", "Traditional"], 100 | "text": "Blue Cheese", 101 | "image": "https://images.unsplash.com/photo-1452195100486-9cc805987862?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjczMTc0fQ&w=400&h=400&fit=fillmax", 102 | "alt": "blue cheese" 103 | }, 104 | { 105 | "combination": ["Austin", "Pasta", "Modern"], 106 | "text": "Cheddar", 107 | "image": "https://images.unsplash.com/photo-1618164436241-4473940d1f5c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2340&q=80", 108 | "alt": "cheddar cheese" 109 | }, 110 | { 111 | "combination": ["Portland", "Sandwich", "Mountains"], 112 | "text": "Feta", 113 | "image": "https://images.unsplash.com/photo-1626957341926-98752fc2ba90?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80", 114 | "alt": "feta" 115 | }, 116 | { 117 | "combination": ["New Orleans", "Hamburger", "House"], 118 | "text": "Halloumi", 119 | "image": "https://images.unsplash.com/photo-1598167912234-02576c0c5f16?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2340&q=80", 120 | "alt": "halloumi" 121 | }, 122 | { 123 | "combination": ["New York", "Pizza", "Modern"], 124 | "text": "Goya", 125 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Queso_Goya.jpg", 126 | "alt": "Goya" 127 | }, 128 | { 129 | "combination": ["New York", "Pizza", "Mountains"], 130 | "text": "Red Hawk", 131 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Cowgirl_Creamery_Point_Reyes_-_Red_Hawk_cheese.jpg", 132 | "alt": "Red Hawk" 133 | }, 134 | { 135 | "combination": ["New York", "Pizza", "House"], 136 | "text": "Pepper Jack", 137 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Pepperjack_Cheese.jpg", 138 | "alt": "Pepper Jack" 139 | }, 140 | { 141 | "combination": ["New York", "Pasta", "Traditional"], 142 | "text": "Nacho cheese", 143 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/Nachos_supreme.jpg/270px-Nachos_supreme.jpg", 144 | "alt": "Nacho cheese" 145 | }, 146 | { 147 | "combination": ["New York", "Pasta", "Modern"], 148 | "text": "Muenster", 149 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Block_of_Muenster_cheese.jpg", 150 | "alt": "Muenster" 151 | }, 152 | { 153 | "combination": ["New York", "Pasta", "Mountains"], 154 | "text": "Humboldt Fog", 155 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Cheese_30_bg_051906.jpg", 156 | "alt": "Humboldt Fog" 157 | }, 158 | { 159 | "combination": ["New York", "Pasta", "House"], 160 | "text": "Brick", 161 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Brickcheese.jpg", 162 | "alt": "Brick" 163 | }, 164 | { 165 | "combination": ["New York", "Sandwich", "Traditional"], 166 | "text": "Oaxaca", 167 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Quesillo_de_Oaxaca.png", 168 | "alt": "Oaxaca" 169 | }, 170 | { 171 | "combination": ["New York", "Sandwich", "Modern"], 172 | "text": "Criollo", 173 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Fromage_criollo._%2815228806466%29.jpg", 174 | "alt": "Criollo" 175 | }, 176 | { 177 | "combination": ["New York", "Sandwich", "Mountains"], 178 | "text": "Asadero", 179 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Tlayuda_con_Quesillo%2C_Oaxaca.jpg", 180 | "alt": "Asadero" 181 | }, 182 | { 183 | "combination": ["New York", "Sandwich", "House"], 184 | "text": "Añejo", 185 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Queso_a%C3%B1ejo.JPG/270px-Queso_a%C3%B1ejo.JPG", 186 | "alt": "Añejo" 187 | }, 188 | { 189 | "combination": ["New York", "Hamburger", "Traditional"], 190 | "text": "Adobera", 191 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Adobera.png/270px-Adobera.png", 192 | "alt": "Adobera" 193 | }, 194 | { 195 | "combination": ["New York", "Hamburger", "Modern"], 196 | "text": "Quesillo", 197 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Quesillo_de_Oaxaca.png/270px-Quesillo_de_Oaxaca.png", 198 | "alt": "Quesillo" 199 | }, 200 | { 201 | "combination": ["New York", "Hamburger", "Mountains"], 202 | "text": "Cuajada", 203 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Cuajada.jpg", 204 | "alt": "Cuajada" 205 | }, 206 | { 207 | "combination": ["New York", "Hamburger", "House"], 208 | "text": "Turrialba", 209 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Queso_Turrialba.jpg", 210 | "alt": "Turrialba" 211 | }, 212 | 213 | { 214 | "combination": ["Austin", "Pizza", "Traditional"], 215 | "text": "Palmito", 216 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Palmito_cheese.jpg", 217 | "alt": "Palmito" 218 | }, 219 | { 220 | "combination": ["Austin", "Pizza", "Modern"], 221 | "text": "Pikauba", 222 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/Pikauba_%28fromage%29_03.jpg/270px-Pikauba_%28fromage%29_03.jpg", 223 | "alt": "Pikauba" 224 | }, 225 | { 226 | "combination": ["Austin", "Pizza", "Mountains"], 227 | "text": "Hellim", 228 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Halloumislicefresh.jpg/270px-Halloumislicefresh.jpg", 229 | "alt": "Hellim" 230 | }, 231 | { 232 | "combination": ["Austin", "Pizza", "House"], 233 | "text": "Beyaz peynir", 234 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/White_cheese_from_Turkey.jpg/270px-White_cheese_from_Turkey.jpg", 235 | "alt": "Beyaz peynir" 236 | }, 237 | { 238 | "combination": ["Austin", "Pasta", "Traditional"], 239 | "text": "Nabulsi", 240 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/%D8%AC%D8%A8%D9%86%D8%A9_%D9%86%D8%A7%D8%A8%D9%84%D8%B3%D9%8A%D8%A9.jpg", 241 | "alt": "Nabulsi" 242 | }, 243 | { 244 | "combination": ["Austin", "Pasta", "Mountains"], 245 | "text": "Jameed", 246 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Jameed.JPG/270px-Jameed.JPG", 247 | "alt": "Jameed" 248 | }, 249 | { 250 | "combination": ["Austin", "Pasta", "House"], 251 | "text": "Labneh", 252 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Labneh01.jpg", 253 | "alt": "Labneh" 254 | }, 255 | { 256 | "combination": ["Austin", "Sandwich", "Traditional"], 257 | "text": "Pot Cheese", 258 | "image": "https://upload.wikimedia.org/wikipedia/commons/7/73/Kupe_paniri.jpg", 259 | "alt": "Pot Cheese" 260 | }, 261 | { 262 | "combination": ["Austin", "Sandwich", "Modern"], 263 | "text": "Rumi", 264 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Flickr_-_Tour_d%27Afrique_-_ArabicCheese.jpg", 265 | "alt": "Rumi" 266 | }, 267 | { 268 | "combination": ["Austin", "Sandwich", "Mountains"], 269 | "text": "Mish", 270 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Mesh.jpg/270px-Mesh.jpg", 271 | "alt": "Mish" 272 | }, 273 | { 274 | "combination": ["Austin", "Sandwich", "House"], 275 | "text": "Domiati", 276 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Domiati_cheese.jpg/270px-Domiati_cheese.jpg", 277 | "alt": "Domiati" 278 | }, 279 | { 280 | "combination": ["Austin", "Hamburger", "Traditional"], 281 | "text": "Bryndza", 282 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Bryndza.jpg/270px-Bryndza.jpg", 283 | "alt": "Bryndza" 284 | }, 285 | { 286 | "combination": ["Austin", "Hamburger", "Modern"], 287 | "text": "Grevé", 288 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Wedge_of_Swedish_Grev%C3%A9_cheese.jpg/270px-Wedge_of_Swedish_Grev%C3%A9_cheese.jpg", 289 | "alt": "Grevé" 290 | }, 291 | { 292 | "combination": ["Austin", "Hamburger", "Mountains"], 293 | "text": "Parenica", 294 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Parenica.jpg/270px-Parenica.jpg", 295 | "alt": "Parenica" 296 | }, 297 | { 298 | "combination": ["Austin", "Hamburger", "House"], 299 | "text": "Korbáčiky", 300 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/%C3%9Aden%C3%BD_korb%C3%A1%C4%8Dik_%28Slovakia%29.jpg/270px-%C3%9Aden%C3%BD_korb%C3%A1%C4%8Dik_%28Slovakia%29.jpg", 301 | "alt": "Korbáčiky" 302 | }, 303 | { 304 | "combination": ["Portland", "Pizza", "Mountains"], 305 | "text": "Liptauer", 306 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Liptauer.jpg/270px-Liptauer.jpg", 307 | "alt": "Liptauer" 308 | }, 309 | { 310 | "combination": ["Portland", "Pizza", "House"], 311 | "text": "Tvorog", 312 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Tvorog.jpg/270px-Tvorog.jpg", 313 | "alt": "Tvorog" 314 | }, 315 | { 316 | "combination": ["Portland", "Pasta", "Traditional"], 317 | "text": "Circassian", 318 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/%D0%90%D0%B4%D1%8B%D0%B3%D0%B5%D0%B9%D1%81%D0%BA%D0%B8%D0%B9_%D1%81%D1%8B%D1%80.jpg", 319 | "alt": "Circassian" 320 | }, 321 | { 322 | "combination": ["Portland", "Pasta", "Modern"], 323 | "text": "Cașcaval", 324 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Penteleu.jpg", 325 | "alt": "Cașcaval" 326 | }, 327 | { 328 | "combination": ["Portland", "Pasta", "Mountains"], 329 | "text": "Telemea", 330 | "image": "https://upload.wikimedia.org/wikipedia/commons/2/29/Telemea.jpg", 331 | "alt": "Telemea" 332 | }, 333 | { 334 | "combination": ["Portland", "Pasta", "House"], 335 | "text": "Requeijão", 336 | "image": "https://upload.wikimedia.org/wikipedia/commons/6/63/RequeijaoCremoso.jpg", 337 | "alt": "Requeijão" 338 | }, 339 | { 340 | "combination": ["Portland", "Sandwich", "Traditional"], 341 | "text": "Norvegia", 342 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Norvegia_Vellagret.JPG", 343 | "alt": "Norvegia" 344 | }, 345 | { 346 | "combination": ["Portland", "Sandwich", "Modern"], 347 | "text": "Jarlsberg", 348 | "image": "https://upload.wikimedia.org/wikipedia/commons/3/36/Jarlsberg_cheese.jpg", 349 | "alt": "Jarlsberg" 350 | }, 351 | { 352 | "combination": ["Portland", "Sandwich", "House"], 353 | "text": "Gamalost", 354 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Gamalost-NorwegianOldCheese.jpg", 355 | "alt": "Gamalost" 356 | }, 357 | { 358 | "combination": ["Portland", "Hamburger", "Traditional"], 359 | "text": "Brunost", 360 | "image": "https://upload.wikimedia.org/wikipedia/commons/c/c5/Brunost.jpg", 361 | "alt": "Brunost" 362 | }, 363 | { 364 | "combination": ["Portland", "Hamburger", "Modern"], 365 | "text": "Pannónia", 366 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Emmental_015.jpg", 367 | "alt": "Pannónia" 368 | }, 369 | { 370 | "combination": ["Portland", "Hamburger", "Mountains"], 371 | "text": "Orda", 372 | "image": "https://upload.wikimedia.org/wikipedia/commons/4/46/Urd%C4%83.png", 373 | "alt": "Orda" 374 | }, 375 | { 376 | "combination": ["Portland", "Hamburger", "House"], 377 | "text": "Trappista", 378 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/TrappistenKaeseOffen_1.jpg", 379 | "alt": "Trappista" 380 | }, 381 | { 382 | "combination": ["New Orleans", "Pizza", "Mountains"], 383 | "text": "Myzithra", 384 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Homemade_Mizithra.jpg/2560px-Homemade_Mizithra.jpg", 385 | "alt": "Myzithra" 386 | }, 387 | { 388 | "combination": ["New Orleans", "Pizza", "House"], 389 | "text": "Metsovone", 390 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/%CE%9C%CE%B5%CF%84%CF%83%CE%BF%CE%B2%CF%8C%CE%BD%CE%B5_6304.jpg", 391 | "alt": "Metsovone" 392 | }, 393 | { 394 | "combination": ["New Orleans", "Pasta", "Traditional"], 395 | "text": "Manouri", 396 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Manouri.jpg", 397 | "alt": "Manouri" 398 | }, 399 | { 400 | "combination": ["New Orleans", "Pasta", "Modern"], 401 | "text": "Aura", 402 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Aura_juusto.jpg", 403 | "alt": "Aura" 404 | }, 405 | { 406 | "combination": ["New Orleans", "Pasta", "Mountains"], 407 | "text": "Saga", 408 | "image": "https://upload.wikimedia.org/wikipedia/commons/d/d0/Soft_and_creamy_Saga_cheese.jpg", 409 | "alt": "Saga" 410 | }, 411 | { 412 | "combination": ["New Orleans", "Pasta", "House"], 413 | "text": "Danish Blue", 414 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Danish_Blue_cheese.jpg", 415 | "alt": "Danish Blue" 416 | }, 417 | { 418 | "combination": ["New Orleans", "Sandwich", "Traditional"], 419 | "text": "Esrom", 420 | "image": "https://upload.wikimedia.org/wikipedia/commons/5/50/Esrom.jpg", 421 | "alt": "Esrom" 422 | }, 423 | { 424 | "combination": ["New Orleans", "Sandwich", "Modern"], 425 | "text": "Staazer", 426 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Staazer_Heublumenk%C3%A4se_SB_01_WikiCheese_Lokal_K.jpg", 427 | "alt": "Staazer" 428 | }, 429 | { 430 | "combination": ["New Orleans", "Sandwich", "Mountains"], 431 | "text": "Kashkaval", 432 | "image": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Kaschkawal_Kashkaval_%D0%BA%D0%B0%D1%88%D0%BA%D0%B0%D0%B2%D0%B0%D0%BB_Balkank%C3%A4se_Sofia_IMG_7649.JPG", 433 | "alt": "Kashkaval" 434 | }, 435 | { 436 | "combination": ["New Orleans", "Sandwich", "House"], 437 | "text": "Sura Kees", 438 | "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Sura-Kees.JPG/1280px-Sura-Kees.JPG", 439 | "alt": "Sura Kees" 440 | }, 441 | { 442 | "combination": ["New Orleans", "Hamburger", "Traditional"], 443 | "text": "Paneer", 444 | "image": "https://images.unsplash.com/photo-1631452180519-c014fe946bc7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80", 445 | "alt": "Paneer" 446 | }, 447 | { 448 | "combination": ["New Orleans", "Hamburger", "Modern"], 449 | "text": "Camembert", 450 | "image": "https://images.unsplash.com/photo-1634487359989-3e90c9432133?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1064&q=80", 451 | "alt": "Camembert" 452 | }, 453 | { 454 | "combination": ["New Orleans", "Hamburger", "Mountains"], 455 | "text": "Sainte-Colome", 456 | "image": "https://images.unsplash.com/photo-1486297678162-eb2a19b0a32d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2073&q=80", 457 | "alt": "sanite colome" 458 | } 459 | ] 460 | } 461 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express' 2 | import axios, { AxiosResponse } from 'axios' 3 | import { QuizData } from './interfaces' 4 | import * as dotenv from 'dotenv' 5 | dotenv.config() 6 | 7 | const PORT = 8000 8 | const app = express() 9 | 10 | app.get('/quiz-item', async (req: Request, res: Response) => { 11 | try { 12 | // @ts-ignore 13 | const response: AxiosResponse = await axios.get(process.env.URL, { 14 | headers: { 15 | 'X-Cassandra-Token': process.env.TOKEN, 16 | accept: 'application/json' 17 | } 18 | }) 19 | if (response.status === 200) { 20 | const quizItem: QuizData = await response.data.data['20bef6ab-f035-4c99-9fcc-3cf19e13b70e'] 21 | res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000') 22 | res.send(quizItem) 23 | } 24 | } catch (err) { 25 | console.error(err) 26 | } 27 | }) 28 | 29 | app.listen(PORT, () => console.log('server is running on port ' + PORT)) 30 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, createRef } from 'react' 2 | import Title from './components/Title' 3 | import QuestionsBlock from "./components/QuestionsBlock" 4 | import AnswerBlock from "./components/AnswerBlock" 5 | import { QuizData, Content } from '../interfaces' 6 | 7 | const App = () => { 8 | const [quiz, setQuiz] = useState() 9 | const [chosenAnswerItems, setChosenAnswerItems] = useState([]) 10 | const [ unansweredQuestionIds, setUnansweredQuestionIds] = useState([]) 11 | const [showAnswer, setShowAnswer] = useState(false) 12 | 13 | type ReduceType = { 14 | id?: {} 15 | } 16 | const refs = unansweredQuestionIds?.reduce((acc, id) => { 17 | acc[id as unknown as keyof ReduceType] = createRef() 18 | return acc 19 | }, {}) 20 | 21 | const answerRef = createRef() 22 | 23 | const fetchData = async () => { 24 | try { 25 | const response = await fetch('http://localhost:8000/quiz-item') 26 | const json = await response.json() 27 | setQuiz(json) 28 | } catch ( err) { 29 | console.error(err) 30 | } 31 | } 32 | 33 | useEffect(() => { 34 | fetchData() 35 | } , []) 36 | 37 | useEffect(() => { 38 | const unansweredIds = quiz?.content?.map(({id} : Content) => id) 39 | setUnansweredQuestionIds(unansweredIds) 40 | }, [quiz]) 41 | 42 | 43 | useEffect(() => { 44 | if (chosenAnswerItems.length > 0 && unansweredQuestionIds) { 45 | if (showAnswer && answerRef.current) { 46 | answerRef.current.scrollIntoView({behavior: 'smooth'}) 47 | } 48 | 49 | if (unansweredQuestionIds.length <= 0 && chosenAnswerItems.length >= 1) { 50 | setShowAnswer(true) 51 | } else { 52 | const highestId = Math.min(...unansweredQuestionIds) 53 | refs[highestId].current.scrollIntoView({behavior: 'smooth'}) 54 | } 55 | } 56 | 57 | 58 | }, [unansweredQuestionIds, chosenAnswerItems.length, showAnswer, answerRef.current, refs]) 59 | 60 | return ( 61 |
62 | 63 | {refs && quiz?.content.map((content: Content) => ( 64 | <QuestionsBlock 65 | key={content.id} 66 | quizItem={content} 67 | chosenAnswerItems={chosenAnswerItems} 68 | setChosenAnswerItems={setChosenAnswerItems} 69 | unansweredQuestionIds={unansweredQuestionIds} 70 | setUnansweredQuestionIds={setUnansweredQuestionIds} 71 | ref={refs[content.id]} 72 | /> 73 | ))} 74 | {showAnswer && 75 | <AnswerBlock 76 | answerOptions={quiz?.answers} 77 | chosenAnswers={chosenAnswerItems} 78 | ref={answerRef} 79 | />} 80 | </div> 81 | ) 82 | } 83 | 84 | export default App 85 | -------------------------------------------------------------------------------- /src/components/AnswerBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, forwardRef} from 'react' 2 | import { Answer } from '../../interfaces' 3 | 4 | const AnswerBlock = ({ 5 | answerOptions, 6 | chosenAnswers 7 | } : { 8 | answerOptions: Answer[] | undefined, 9 | chosenAnswers: string[] 10 | }, ref: HTMLDivElement | any) => { 11 | const [result, setResult] = useState<Answer| null>() 12 | 13 | useEffect(() => { 14 | answerOptions?.forEach((answer: Answer) => { 15 | if ( 16 | chosenAnswers.includes(answer.combination[0]) && 17 | chosenAnswers.includes(answer.combination[1]) && 18 | chosenAnswers.includes(answer.combination[2]) 19 | ) { 20 | setResult(answer) 21 | } 22 | }) 23 | }, [chosenAnswers]) 24 | 25 | console.log(result) 26 | 27 | return ( 28 | <div ref={ref} className="answer-block"> 29 | <h2>{result?.text}</h2> 30 | <img src={result?.image} alt={result?.text}/> 31 | </div> 32 | ) 33 | } 34 | 35 | export default forwardRef(AnswerBlock) 36 | -------------------------------------------------------------------------------- /src/components/QuestionBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Question } from '../../interfaces' 3 | 4 | const QuestionBlock = ({ 5 | quizItemId, 6 | question, 7 | chosenAnswerItems, 8 | setChosenAnswerItems, 9 | unansweredQuestionIds, 10 | setUnansweredQuestionIds 11 | }: { 12 | quizItemId: number; 13 | question: Question, 14 | chosenAnswerItems: string[], 15 | setChosenAnswerItems: Function, 16 | unansweredQuestionIds: number[] | undefined, 17 | setUnansweredQuestionIds: Function 18 | }) => { 19 | 20 | 21 | const handleClick = () => { 22 | setChosenAnswerItems((prevState: string[]) => [...prevState, question.text]) 23 | setUnansweredQuestionIds(unansweredQuestionIds?.filter((id: number) => id !== quizItemId)) 24 | } 25 | 26 | const validPick = !chosenAnswerItems?.includes(question.text) && 27 | !unansweredQuestionIds?.includes(quizItemId) 28 | 29 | 30 | return ( 31 | <button 32 | className="question-block" 33 | onClick={handleClick} 34 | disabled={validPick} 35 | > 36 | <img src={question.image} alt={question.alt}/> 37 | <h3>{question.text}</h3> 38 | <p> 39 | <a href={question.image}>{question.credit} </a> 40 | <a href="https://www.unsplash.com">Unsplash</a> 41 | </p> 42 | 43 | </button> 44 | ) 45 | } 46 | 47 | export default QuestionBlock 48 | -------------------------------------------------------------------------------- /src/components/QuestionsBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { Content, Question } from '../../interfaces' 3 | import QuestionBlock from './QuestionBlock' 4 | 5 | const QuestionsBlock = ({ 6 | quizItem, 7 | chosenAnswerItems, 8 | setChosenAnswerItems, 9 | unansweredQuestionIds, 10 | setUnansweredQuestionIds, 11 | } : { 12 | quizItem: Content, 13 | chosenAnswerItems: string[], 14 | setChosenAnswerItems: Function, 15 | unansweredQuestionIds: number[] | undefined 16 | setUnansweredQuestionIds: Function 17 | }, ref: React.LegacyRef<HTMLHeadingElement> | undefined ) => { 18 | return ( 19 | <> 20 | <h2 ref={ref} className="title-block" >{quizItem.text}</h2> 21 | <div className="questions-container"> 22 | {quizItem?.questions.map((question: Question, _index: number) =>( 23 | <QuestionBlock 24 | key={_index} 25 | quizItemId={quizItem.id} 26 | question={question} 27 | chosenAnswerItems={chosenAnswerItems} 28 | setChosenAnswerItems={setChosenAnswerItems} 29 | unansweredQuestionIds={unansweredQuestionIds} 30 | setUnansweredQuestionIds={setUnansweredQuestionIds} 31 | /> 32 | ))} 33 | </div> 34 | </> 35 | ) 36 | } 37 | 38 | export default forwardRef(QuestionsBlock) 39 | -------------------------------------------------------------------------------- /src/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { QuizData } from "../../interfaces" 3 | 4 | const Title = ({ title, subtitle } : { 5 | title: QuizData['title'] | undefined, 6 | subtitle: QuizData['subtitle'] | undefined, 7 | }) => { 8 | return ( 9 | <div> 10 | <h1>{title}</h1> 11 | <p>{subtitle}</p> 12 | </div> 13 | ) 14 | } 15 | 16 | export default Title 17 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;400;500&display=swap'); 2 | 3 | * { 4 | font-family: 'Montserrat', sans-serif; 5 | } 6 | 7 | body { 8 | display: flex; 9 | justify-content: center; 10 | } 11 | 12 | .app { 13 | width: 37.5rem; 14 | } 15 | 16 | h1 { 17 | font-size: 2.5rem; 18 | line-height: 1.05; 19 | text-align: left; 20 | } 21 | 22 | h2 { 23 | font-size: 3.5rem; 24 | line-height: 1.05; 25 | text-align: center; 26 | } 27 | 28 | h3 { 29 | margin: 0.625rem; 30 | } 31 | 32 | p { 33 | font-size: 1.125rem; 34 | line-height: 1.2; 35 | } 36 | 37 | .title-block { 38 | width: 37.5rem; 39 | height: 13.125rem; 40 | background-color: rgb(102, 69, 221); 41 | border-radius: 0.313rem; 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | color: rgb(255,255,255); 46 | } 47 | 48 | .questions-container { 49 | display: flex; 50 | flex-wrap: wrap; 51 | justify-content: space-between; 52 | } 53 | 54 | .question-block { 55 | width: 17.875rem; 56 | overflow: hidden; 57 | background-color: rgb(255,255,255); 58 | border-radius: 0.313rem; 59 | box-shadow: rgba(0,0,0,0.07) 0 0 0 0.063rem; 60 | text-align: center; 61 | margin-bottom: 0.938rem; 62 | border: none; 63 | padding: 0; 64 | } 65 | 66 | .question-block p { 67 | font-size: 0.8rem; 68 | font-style: italic; 69 | } 70 | 71 | .question-block a { 72 | color: rgb(135, 135, 135); 73 | text-decoration: none; 74 | } 75 | 76 | .answer-block { 77 | width: 36.5rem; 78 | background-color: rgb(255,61,158); 79 | border-radius: 0.313rem; 80 | display: flex; 81 | align-items: center; 82 | flex-direction: column; 83 | color: rgb(255,255,255); 84 | } 85 | 86 | .answer-block img { 87 | width: 90%; 88 | } 89 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import './index.css' 4 | import App from './App' 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById('root') as HTMLElement 8 | ) 9 | root.render( 10 | <React.StrictMode> 11 | <App /> 12 | </React.StrictMode> 13 | ) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "commonjs", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------