├── .eslintrc.json ├── .gitignore ├── README.md ├── babel.config.json ├── codecept.conf.js ├── e2e ├── Liking_Outlet.spec.js ├── Review_Outlet.spec.js ├── Unliking_Outlet.spec.js └── outputs │ ├── Give_Review_Feedback.failed.png │ ├── Unliking_Outlet_before_each_hook__Before_for_Display_Favorite_Outlet.failed.png │ └── liking_an_outlet.failed.png ├── jsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── specs ├── contract │ └── favoriteOutletContract.js ├── favoriteOutletArraySpec.js ├── favoriteOutletDbSpec.js ├── favoriteOutletSearchPresenterSpec.js ├── favoriteOutletShowSpec.js ├── helpers │ └── testFactory.js ├── likeOutletSpec.js └── unlikeOuletSpec.js ├── src ├── public │ ├── icons-set │ │ ├── arrow.svg │ │ ├── chart.svg │ │ ├── heart-regular.svg │ │ ├── heart-solid.svg │ │ ├── location.svg │ │ ├── logo.svg │ │ ├── star.svg │ │ └── white-location.svg │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ ├── images │ │ ├── 404.jpg │ │ ├── data.jpg │ │ ├── hero-large.jpg │ │ ├── hero-small.jpg │ │ ├── hero.jpg │ │ ├── page.jpg │ │ ├── placeholder.png │ │ └── profile.jpg │ └── manifest.json ├── scripts │ ├── components │ │ ├── AllFood.js │ │ ├── Choose.js │ │ ├── ExcessSection.js │ │ ├── FavoriteNotFound.js │ │ ├── Footer.js │ │ ├── Loading.js │ │ ├── Main.js │ │ ├── MostFood.js │ │ ├── Navbar.js │ │ ├── Outlet.js │ │ └── detail-component │ │ │ ├── ButtonContainer.js │ │ │ ├── DetailOutlet.js │ │ │ ├── FormContainer.js │ │ │ ├── Menu.js │ │ │ ├── NotFound.js │ │ │ └── Review.js │ ├── data │ │ ├── data-outlet.js │ │ └── favorite-outlet.js │ ├── global │ │ ├── api-endpoint.js │ │ └── config.js │ ├── index.js │ ├── json │ │ └── BITES.json │ ├── routes │ │ ├── routes.js │ │ └── url-parser.js │ ├── sw.js │ ├── utils │ │ ├── arrow-animation.js │ │ ├── check-online.js │ │ ├── close-drawer.js │ │ ├── drawer-initiator.js │ │ ├── form-input-handler.js │ │ ├── form-validation.js │ │ ├── hamburger-action.js │ │ ├── like-button-presenter.js │ │ ├── loading-initiator.js │ │ └── sw-register.js │ └── views │ │ ├── app.js │ │ ├── pages │ │ ├── detail.js │ │ ├── favorite-outlet │ │ │ ├── favorite-outlet-search-presenter.js │ │ │ ├── favorite-outlet-search-view.js │ │ │ └── favorite-outlet-show-presenter.js │ │ ├── favorite.js │ │ ├── food.js │ │ ├── home.js │ │ └── outlet.js │ │ └── templates │ │ ├── api-template.js │ │ └── local-template.js ├── styles │ ├── detail.css │ ├── loading.css │ ├── responsive.css │ ├── skeleton.css │ └── style.css └── templates │ └── index.html ├── steps.d.ts ├── steps_file.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["airbnb-base"], 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "linebreak-style": "off", 13 | "indent": "off", 14 | "no-tabs": "off", 15 | "import/no-extraneous-dependencies": "off", 16 | "no-console": "off", 17 | "no-underscore-dangle": "off", 18 | "no-unused-vars": "off", 19 | "arrow-body-style": "off", 20 | "dot-notation": "off", 21 | "no-shadow": "off", 22 | "array-callback-return": "off", 23 | "prefer-destructuring": "off", 24 | "operator-linebreak": "off", 25 | "no-restricted-globals": "off", 26 | "import/order": "off", 27 | "comma-dangle": "off", 28 | "func-names": "off", 29 | "no-var": "off", 30 | "class-methods-use-this": "off", 31 | "no-mixed-spaces-and-tabs": "off", 32 | "no-unused-expressions": "off", 33 | "no-useless-concat": "off", 34 | "consistent-return": "off", 35 | "no-return-assign": "off", 36 | "no-prototype-builtins": "off", 37 | "no-new": "off", 38 | "object-curly-newline": "off", 39 | "no-undef": "off", 40 | "import/prefer-default-export": "off", 41 | "max-len": "off" 42 | }, 43 | "plugins": ["jasmine", "codeceptjs"], 44 | "parser": "@babel/eslint-parser" 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bites 2 | 3 | Katalog Outlet Restaurant 4 | 5 | Tech Stack : 6 | - HTML, CSS, JS 7 | - Eslint 8 | - PWA 9 | - Webpack 10 | - IndexedDB 11 | - Workbox 12 | - Karma & Jasmine 13 | - Codecept & Pupeteer 14 | - SweetAlert 15 | 16 | Link Project : https://bitess.netlify.app/ 17 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "edge": "17", 8 | "firefox": "60", 9 | "chrome": "67", 10 | "safari": "11.1" 11 | }, 12 | "useBuiltIns": "usage", 13 | "corejs": "3.6.5" 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /codecept.conf.js: -------------------------------------------------------------------------------- 1 | const { setHeadlessWhen } = require('@codeceptjs/configure'); 2 | 3 | // turn on headless mode when running with HEADLESS=true environment variable 4 | // export HEADLESS=true && npx codeceptjs run 5 | setHeadlessWhen(process.env.HEADLESS); 6 | 7 | exports.config = { 8 | tests: 'e2e/**/*.spec.js', 9 | output: 'e2e/outputs', 10 | helpers: { 11 | Puppeteer: { 12 | url: 'http://localhost:8080', 13 | show: true, 14 | windowSize: '1200x900' 15 | } 16 | }, 17 | include: { 18 | I: './steps_file.js' 19 | }, 20 | bootstrap: null, 21 | mocha: {}, 22 | name: 'bites-v2', 23 | plugins: { 24 | pauseOnFail: {}, 25 | retryFailedStep: { 26 | enabled: true 27 | }, 28 | tryTo: { 29 | enabled: true 30 | }, 31 | screenshotOnFail: { 32 | enabled: true 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /e2e/Liking_Outlet.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | Feature('Favorite Outlets'); 4 | 5 | Before(({ I }) => { 6 | I.amOnPage('/#/favorite'); 7 | }); 8 | 9 | Scenario('showing empty liked outlet', ({ I }) => { 10 | I.seeElement('.notfound'); 11 | I.see("You haven't chosen your favorite outlet", '.notfound-title'); 12 | I.wait(3); 13 | }); 14 | 15 | Scenario('liking an outlet', async ({ I }) => { 16 | I.see("You haven't chosen your favorite outlet", '.notfound-title'); 17 | 18 | // Navigate to the outlet page 19 | I.amOnPage('/#/outlet'); 20 | 21 | // See oulet list in the outlet page 22 | I.seeElement('#outletName a'); 23 | 24 | const firstOutlet = locate('#outletName a').first(); 25 | const firstOutletName = await I.grabTextFrom(firstOutlet); 26 | 27 | // Simulate user click on the first outlet 28 | I.click(firstOutlet); 29 | 30 | // See datail outlet and simulate user click on the favorite button 31 | // I.seeElement('#mainContent'); 32 | I.seeElement('.detail-outlet-section'); 33 | I.seeElement('#likeButton'); 34 | I.click('#likeButton'); 35 | 36 | // After user click on the favorite button, display notification 37 | I.see('You have favorite outlet now!', '.swal2-popup'); 38 | 39 | // Navigate to the favorite page 40 | I.amOnPage('/#/favorite'); 41 | I.seeElement('#outletName a'); 42 | 43 | const favoritedOutletName = await I.grabTextFrom('#outletName a'); 44 | 45 | // Outlet name should be the same as the one in the outlet page 46 | assert.strictEqual(firstOutletName, favoritedOutletName); 47 | }); 48 | -------------------------------------------------------------------------------- /e2e/Review_Outlet.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | Feature('Review Outlet'); 4 | 5 | Before(({ I }) => { 6 | I.amOnPage('/#/outlet'); 7 | 8 | const outlet = locate('#outletName a').at(3); 9 | I.click(outlet); 10 | I.wait(3); 11 | }); 12 | 13 | Scenario('Give Review Feedback', async ({ I }) => { 14 | const reviewName = 'Reviewer'; 15 | const reviewDetail = 'Reviewer Detail'; 16 | 17 | I.seeElement('#form-container'); 18 | 19 | I.fillField('#reviewName', reviewName); 20 | I.fillField('#reviewDetail', reviewDetail); 21 | I.click('#submit'); 22 | 23 | I.wait(2); 24 | 25 | I.see('Successfully added review', '.swal2-popup'); 26 | I.wait(2); 27 | I.click('.swal2-confirm'); 28 | 29 | I.wait(3); 30 | 31 | const getReviewerName = await I.grabTextFrom(locate('.name').last()); 32 | const getReviewerDetail = await I.grabTextFrom(locate('.review-text').last()); 33 | 34 | assert.strictEqual(reviewName, getReviewerName); 35 | assert.strictEqual(reviewDetail, getReviewerDetail); 36 | }); 37 | -------------------------------------------------------------------------------- /e2e/Unliking_Outlet.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | Feature('Unliking Outlet'); 4 | 5 | let firstOutletName; 6 | 7 | Before(async ({ I }) => { 8 | I.amOnPage('/#/outlet'); 9 | 10 | I.seeElement('#outletName a'); 11 | const firstOutlet = locate('#outletName a').first(); 12 | firstOutletName = await I.grabTextFrom(firstOutlet); 13 | 14 | I.click(firstOutlet); 15 | 16 | I.seeElement('#likeButton'); 17 | 18 | I.click('#likeButton'); 19 | I.amOnPage('/#/favorite'); 20 | 21 | I.wait(3); 22 | }); 23 | 24 | Scenario('Display Favorite Outlet', async ({ I }) => { 25 | I.seeElement('.outlet'); 26 | const favoriteOutletName = await I.grabTextFrom('#outletName a'); 27 | 28 | assert.strictEqual(firstOutletName, favoriteOutletName); 29 | I.wait(3); 30 | }); 31 | 32 | Scenario('Unliking Outlet From Favorite Outlet', ({ I }) => { 33 | I.amOnPage('/#/favorite'); 34 | I.seeElement('#outletName a'); 35 | 36 | const firstOutlet = locate('#outletName a').first(); 37 | I.click(firstOutlet); 38 | 39 | I.seeElement('#likeButton'); 40 | I.click('#likeButton'); 41 | 42 | I.see('Outlet has been removed!', '.swal2-popup'); 43 | 44 | I.amOnPage('/#/favorite'); 45 | I.see("You haven't chosen your favorite outlet", '.notfound-title'); 46 | }); 47 | -------------------------------------------------------------------------------- /e2e/outputs/Give_Review_Feedback.failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/e2e/outputs/Give_Review_Feedback.failed.png -------------------------------------------------------------------------------- /e2e/outputs/Unliking_Outlet_before_each_hook__Before_for_Display_Favorite_Outlet.failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/e2e/outputs/Unliking_Outlet_before_each_hook__Before_for_Display_Favorite_Outlet.failed.png -------------------------------------------------------------------------------- /e2e/outputs/liking_an_outlet.failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/e2e/outputs/liking_an_outlet.failed.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Nov 30 2021 00:39:27 GMT+0700 (Western Indonesia Time) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | // frameworks to use 10 | // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: ['specs/**/*Spec.js'], 15 | 16 | // list of files / patterns to exclude 17 | exclude: [], 18 | 19 | // preprocess matching files before serving them to the browser 20 | // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 21 | preprocessors: { 'specs/**/*Spec.js': ['webpack', 'sourcemap'] }, 22 | 23 | webpack: { 24 | // karma watches the test entry points 25 | // (you don't need to specify the entry option) 26 | // webpack watches dependencies 27 | // webpack configuration 28 | devtool: 'inline-source-map', 29 | mode: 'development', 30 | }, 31 | 32 | webpackMiddleware: { 33 | // webpack-dev-middleware configuration 34 | // i. e. 35 | stats: 'errors-only', 36 | }, 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 41 | reporters: ['progress'], 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | // level of logging 50 | logLevel: config.LOG_INFO, 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: true, 54 | 55 | // start these browsers 56 | // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 57 | browsers: ['Chrome'], 58 | 59 | // Continuous Integration mode 60 | // if true, Karma captures browsers, runs the tests and exits 61 | singleRun: false, 62 | 63 | // Concurrency level 64 | // how many browser instances should be started simultaneously 65 | concurrency: Infinity, 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bites-v2", 3 | "version": "1.0.0", 4 | "description": "Bites is the website of a restaurant called Bites which is very famous in Indonesia", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --config webpack.dev.js", 8 | "build": "webpack --config webpack.prod.js", 9 | "test": "karma start", 10 | "e2e": "codeceptjs run --steps" 11 | }, 12 | "author": "Rolando Pranata", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@babel/cli": "^7.16.0", 16 | "@babel/core": "^7.16.0", 17 | "@babel/eslint-parser": "^7.16.3", 18 | "@babel/preset-env": "^7.10.4", 19 | "babel-loader": "^8.1.0", 20 | "codeceptjs": "^3.2.2", 21 | "copy-webpack-plugin": "^6.4.1", 22 | "css-loader": "^3.6.0", 23 | "eslint": "^7.32.0", 24 | "eslint-config-airbnb-base": "^14.2.1", 25 | "eslint-plugin-codeceptjs": "^1.3.0", 26 | "eslint-plugin-import": "^2.25.2", 27 | "eslint-plugin-jasmine": "^4.1.3", 28 | "html-webpack-plugin": "^4.5.2", 29 | "imagemin-webp-webpack-plugin": "^3.3.6", 30 | "jasmine-core": "^3.10.1", 31 | "karma": "^5.1.0", 32 | "karma-chrome-launcher": "^3.1.0", 33 | "karma-jasmine": "^3.3.1", 34 | "karma-sourcemap-loader": "^0.3.7", 35 | "karma-webpack": "^4.0.2", 36 | "puppeteer": "^12.0.1", 37 | "style-loader": "^1.2.1", 38 | "webpack": "^4.43.0", 39 | "webpack-bundle-analyzer": "^4.5.0", 40 | "webpack-cli": "^3.3.12", 41 | "webpack-dev-server": "^3.11.0", 42 | "webpack-merge": "^5.0.9", 43 | "webpack-pwa-manifest": "^4.3.0", 44 | "workbox-webpack-plugin": "^6.3.0" 45 | }, 46 | "dependencies": { 47 | "file-loader": "^6.2.0", 48 | "idb": "^6.1.5", 49 | "lazysizes": "^5.3.2", 50 | "regenerator-runtime": "^0.13.9", 51 | "sweetalert2": "^11.2.2", 52 | "url-loader": "^4.1.1", 53 | "workbox-core": "^6.0.2", 54 | "workbox-expiration": "^6.0.2", 55 | "workbox-precaching": "^6.0.2", 56 | "workbox-routing": "^6.0.2", 57 | "workbox-strategies": "^6.0.2", 58 | "workbox-window": "^6.0.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /specs/contract/favoriteOutletContract.js: -------------------------------------------------------------------------------- 1 | const itActsFavoriteOutlet = (favoriteOutlet) => { 2 | it('should return the outlet that has been added', async () => { 3 | favoriteOutlet.putOutlet({ id: 1 }); 4 | favoriteOutlet.putOutlet({ id: 2 }); 5 | 6 | expect(await favoriteOutlet.getOutlet(1)).toEqual({ id: 1 }); 7 | expect(await favoriteOutlet.getOutlet(2)).toEqual({ id: 2 }); 8 | expect(await favoriteOutlet.getOutlet(3)).toEqual(undefined); 9 | }); 10 | 11 | it('should refuse a outlet from being added if it does not have the correct property', async () => { 12 | favoriteOutlet.putOutlet({ aProperty: 'property' }); 13 | 14 | expect(await favoriteOutlet.getAllOutlets()).toEqual([]); 15 | }); 16 | 17 | it('can return all of the outlet that have been added', async () => { 18 | favoriteOutlet.putOutlet({ id: 1 }); 19 | favoriteOutlet.putOutlet({ id: 2 }); 20 | 21 | expect(await favoriteOutlet.getAllOutlets()).toEqual([ 22 | { id: 1 }, 23 | { id: 2 }, 24 | ]); 25 | }); 26 | 27 | it('should remove favorite outlet', async () => { 28 | favoriteOutlet.putOutlet({ id: 1 }); 29 | favoriteOutlet.putOutlet({ id: 2 }); 30 | favoriteOutlet.putOutlet({ id: 3 }); 31 | 32 | await favoriteOutlet.deleteOutlet(1); 33 | 34 | expect(await favoriteOutlet.getAllOutlets()).toEqual([ 35 | { id: 2 }, 36 | { id: 3 }, 37 | ]); 38 | }); 39 | 40 | it('should handle request to remove a outlet even thought the outlet has not been added', async () => { 41 | favoriteOutlet.putOutlet({ id: 1 }); 42 | favoriteOutlet.putOutlet({ id: 2 }); 43 | favoriteOutlet.putOutlet({ id: 3 }); 44 | 45 | await favoriteOutlet.deleteOutlet(4); 46 | 47 | expect(await favoriteOutlet.getAllOutlets()).toEqual([ 48 | { id: 1 }, 49 | { id: 2 }, 50 | { id: 3 }, 51 | ]); 52 | }); 53 | 54 | it('should be able to search for outlet', async () => { 55 | favoriteOutlet.putOutlet({ id: 1, name: 'outlet a' }); 56 | favoriteOutlet.putOutlet({ id: 2, name: 'outlet b' }); 57 | favoriteOutlet.putOutlet({ id: 3, name: 'outlet abc' }); 58 | favoriteOutlet.putOutlet({ id: 4, name: 'outlet abcd' }); 59 | 60 | expect(await favoriteOutlet.searchOutlet('outlet a')).toEqual([ 61 | { id: 1, name: 'outlet a' }, 62 | { id: 3, name: 'outlet abc' }, 63 | { id: 4, name: 'outlet abcd' }, 64 | ]); 65 | }); 66 | }; 67 | 68 | export default itActsFavoriteOutlet; 69 | -------------------------------------------------------------------------------- /specs/favoriteOutletArraySpec.js: -------------------------------------------------------------------------------- 1 | import itActsFavoriteOutlet from './contract/favoriteOutletContract'; 2 | 3 | let favoriteOutletArray = []; 4 | 5 | const FavoriteOutletArray = { 6 | getOutlet(id) { 7 | if (!id) return; 8 | return favoriteOutletArray.find((outlet) => outlet.id === id); 9 | }, 10 | 11 | getAllOutlets() { 12 | return favoriteOutletArray; 13 | }, 14 | 15 | putOutlet(outlet) { 16 | // If outlet not have id, return false 17 | if (!outlet.hasOwnProperty('id')) return; 18 | 19 | if (this.getOutlet(outlet.id)) return; 20 | 21 | favoriteOutletArray.push(outlet); 22 | }, 23 | 24 | deleteOutlet(id) { 25 | // delete outlet if outlet not have the same id equal to oulet.id using filter 26 | favoriteOutletArray = favoriteOutletArray.filter( 27 | (outlet) => outlet.id !== id 28 | ); 29 | }, 30 | 31 | searchOutlet(query) { 32 | return this.getAllOutlets().filter((outlet) => { 33 | const loweredCaseOutletName = (outlet.name || '-').toLowerCase(); 34 | const OutletName = loweredCaseOutletName.replace(/\s/g, ''); 35 | 36 | const loweredCaseQuery = query.toLowerCase(); 37 | const Query = loweredCaseQuery.replace(/\s/g, ''); 38 | 39 | return OutletName.indexOf(Query) !== -1; 40 | }); 41 | }, 42 | }; 43 | 44 | describe('Favorite Outlet Array Contract Test Implementation', () => { 45 | afterEach(() => (favoriteOutletArray = [])); 46 | 47 | itActsFavoriteOutlet(FavoriteOutletArray); 48 | }); 49 | -------------------------------------------------------------------------------- /specs/favoriteOutletDbSpec.js: -------------------------------------------------------------------------------- 1 | import itActsFavoriteOutlet from './contract/favoriteOutletContract'; 2 | import FavoriteOutletDatabase from '../src/scripts/data/favorite-outlet'; 3 | 4 | describe('Favorite Outlet DB Contranct Test Implementation', () => { 5 | // Running deleteOutlet after test will be done. 6 | afterEach(async () => { 7 | (await FavoriteOutletDatabase.getAllOutlets()).forEach(async (outlet) => { 8 | await FavoriteOutletDatabase.deleteOutlet(outlet.id); 9 | }); 10 | }); 11 | 12 | // Call model test for FavoriteOutletDatabase after remove outlet from database. 13 | itActsFavoriteOutlet(FavoriteOutletDatabase); 14 | }); 15 | -------------------------------------------------------------------------------- /specs/favoriteOutletSearchPresenterSpec.js: -------------------------------------------------------------------------------- 1 | import FavoriteOutletSearchPresenter from '../src/scripts/views/pages/favorite-outlet/favorite-outlet-search-presenter'; 2 | import FavoriteOutletDatabase from '../src/scripts/data/favorite-outlet'; 3 | import FavoriteOutletSearchView from '../src/scripts/views/pages/favorite-outlet/favorite-outlet-search-view'; 4 | 5 | describe('Searching Outlet', () => { 6 | let presenter; 7 | let favoriteOutlet; 8 | let view; 9 | 10 | const searchOutlet = (query) => { 11 | const queryelement = document.getElementById('query'); 12 | queryelement.value = query; 13 | queryelement.dispatchEvent(new Event('change')); 14 | }; 15 | 16 | const setOutletSearchContainer = () => { 17 | view = new FavoriteOutletSearchView(); 18 | document.body.innerHTML = view.getTemplate(); 19 | }; 20 | 21 | const constructPresenter = () => { 22 | favoriteOutlet = spyOnAllFunctions(FavoriteOutletDatabase); 23 | presenter = new FavoriteOutletSearchPresenter({ 24 | favoriteOutlet, 25 | view, 26 | }); 27 | }; 28 | 29 | beforeEach(() => { 30 | setOutletSearchContainer(); 31 | constructPresenter(); 32 | }); 33 | 34 | describe('When query is not empty', () => { 35 | it('should be able to capture the query typed by the user', () => { 36 | searchOutlet('outlet a'); 37 | expect(presenter.latestQuery).toEqual('outlet a'); 38 | }); 39 | 40 | it('should ask the model to search for liked outlet', () => { 41 | searchOutlet('outlet a'); 42 | expect(favoriteOutlet.searchOutlet).toHaveBeenCalledWith('outlet a'); 43 | }); 44 | 45 | it('should show outlet not found when the outlet returned does not contain a title', (done) => { 46 | document 47 | .getElementById('outlet') 48 | .addEventListener('outlet:updated', () => { 49 | const outletName = document.querySelectorAll('.outlet-name'); 50 | expect(outletName.item(0).textContent).toEqual('outlet not found'); 51 | 52 | done(); 53 | }); 54 | 55 | favoriteOutlet.searchOutlet 56 | .withArgs('outlet a') 57 | .and.returnValues([{ id: 33 }]); 58 | 59 | searchOutlet('outlet a'); 60 | }); 61 | 62 | it('should show the outlet found by Favorite Outlets', (done) => { 63 | document 64 | .getElementById('outlet') 65 | .addEventListener('outlet:updated', () => { 66 | expect(document.querySelectorAll('.outlet-name').length).toEqual(3); 67 | done(); 68 | }); 69 | 70 | favoriteOutlet.searchOutlet.withArgs('outlet a').and.returnValues([ 71 | { id: 111, name: 'outlet abc' }, 72 | { id: 222, name: 'outlet def' }, 73 | { id: 333, name: 'outlet ghi' }, 74 | ]); 75 | 76 | searchOutlet('outlet a'); 77 | }); 78 | 79 | it('should show the name of the outlet found be Favorite Outlet', (done) => { 80 | document 81 | .getElementById('outlet') 82 | .addEventListener('outlet:updated', () => { 83 | const outletTitles = document.querySelectorAll('.outlet-name'); 84 | expect(outletTitles.item(0).textContent).toEqual('outlet abc'); 85 | expect(outletTitles.item(1).textContent).toEqual('outlet def'); 86 | expect(outletTitles.item(2).textContent).toEqual('outlet ghi'); 87 | 88 | done(); 89 | }); 90 | 91 | favoriteOutlet.searchOutlet.withArgs('outlet a').and.returnValues([ 92 | { id: 111, name: 'outlet abc' }, 93 | { id: 222, name: 'outlet def' }, 94 | { id: 333, name: 'outlet ghi' }, 95 | ]); 96 | 97 | searchOutlet('outlet a'); 98 | }); 99 | }); 100 | 101 | describe('When query is empty', () => { 102 | it('should capture the query as empty', () => { 103 | searchOutlet(' '); 104 | expect(presenter.latestQuery.length).toEqual(0); 105 | 106 | searchOutlet(' '); 107 | expect(presenter.latestQuery.length).toEqual(0); 108 | 109 | searchOutlet(''); 110 | expect(presenter.latestQuery.length).toEqual(0); 111 | 112 | searchOutlet('\t'); 113 | expect(presenter.latestQuery.length).toEqual(0); 114 | }); 115 | 116 | it('should show all favorite outlet', () => { 117 | searchOutlet(' '); 118 | 119 | // Using toHaveBeenCalled for make sure getAllOutlets is callled 120 | expect(favoriteOutlet.getAllOutlets).toHaveBeenCalled(); 121 | }); 122 | }); 123 | 124 | describe('When no favorite outlet could be found', () => { 125 | it('should show the empty message', (done) => { 126 | document 127 | .getElementById('outlet') 128 | .addEventListener('outlet:updated', () => { 129 | expect(document.querySelectorAll('.notfound').length).toEqual(1); 130 | done(); 131 | }); 132 | favoriteOutlet.searchOutlet.withArgs('outlet a').and.returnValues([]); 133 | searchOutlet('outlet a'); 134 | }); 135 | 136 | it('should not show any outlet', (done) => { 137 | document 138 | .getElementById('outlet') 139 | .addEventListener('outlet:updated', () => { 140 | expect(document.querySelectorAll('.outlet-card').length).toEqual(0); 141 | done(); 142 | }); 143 | 144 | favoriteOutlet.searchOutlet.withArgs('outlet a').and.returnValues([]); 145 | 146 | searchOutlet('outlet a'); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /specs/favoriteOutletShowSpec.js: -------------------------------------------------------------------------------- 1 | import FavoriteOutletSearchView from '../src/scripts/views/pages/favorite-outlet/favorite-outlet-search-view'; 2 | import FavoriteOutletShowPresenter from '../src/scripts/views/pages/favorite-outlet/favorite-outlet-show-presenter'; 3 | import FavoriteOutletDatabase from '../src/scripts/data/favorite-outlet'; 4 | 5 | describe('Showing all favorite outlet', () => { 6 | let view; 7 | 8 | const renderTemplate = () => { 9 | view = new FavoriteOutletSearchView(); 10 | document.body.innerHTML = view.getTemplate(); 11 | }; 12 | 13 | beforeEach(() => { 14 | renderTemplate(); 15 | }); 16 | 17 | describe('When no outlet have been liked', () => { 18 | it('should ask for the favorite outlet', () => { 19 | const favoriteOutlet = spyOnAllFunctions(FavoriteOutletDatabase); 20 | 21 | new FavoriteOutletShowPresenter({ 22 | view, 23 | favoriteOutlet, 24 | }); 25 | 26 | expect(favoriteOutlet.getAllOutlets).toHaveBeenCalledTimes(1); 27 | }); 28 | 29 | it('should show the information that no outlet have been liked', (done) => { 30 | document 31 | .getElementById('outlet') 32 | .addEventListener('outlet:updated', () => { 33 | expect(document.querySelectorAll('.notfound').length).toEqual(1); 34 | 35 | done(); 36 | }); 37 | 38 | const favoriteOutlet = spyOnAllFunctions(FavoriteOutletDatabase); 39 | favoriteOutlet.getAllOutlets.and.returnValue([]); 40 | 41 | new FavoriteOutletShowPresenter({ 42 | view, 43 | favoriteOutlet, 44 | }); 45 | }); 46 | }); 47 | 48 | describe('When favorite outlet exist', () => { 49 | it('should show the outlet', (done) => { 50 | document 51 | .getElementById('outlet') 52 | .addEventListener('outlet:updated', () => { 53 | expect(document.querySelectorAll('.outlet-card').length).toEqual(2); 54 | done(); 55 | }); 56 | 57 | const favoriteOutlet = spyOnAllFunctions(FavoriteOutletDatabase); 58 | favoriteOutlet.getAllOutlets.and.returnValue([ 59 | { 60 | id: 11, 61 | name: 'Outlet a', 62 | }, 63 | { 64 | id: 22, 65 | name: 'Outlet b', 66 | }, 67 | ]); 68 | 69 | new FavoriteOutletShowPresenter({ 70 | view, 71 | favoriteOutlet, 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /specs/helpers/testFactory.js: -------------------------------------------------------------------------------- 1 | import LikeButtonPresenter from '../../src/scripts/utils/like-button-presenter'; 2 | import FavoriteOutletDatabase from '../../src/scripts/data/favorite-outlet'; 3 | 4 | const createLikeButtonPresenterWithOutlet = async (outlet) => { 5 | await LikeButtonPresenter.init({ 6 | likeButtonContainer: document.querySelector('#likeButtonContainer'), 7 | favoriteOutlet: FavoriteOutletDatabase, 8 | outlet, 9 | }); 10 | }; 11 | 12 | export { createLikeButtonPresenterWithOutlet }; 13 | -------------------------------------------------------------------------------- /specs/likeOutletSpec.js: -------------------------------------------------------------------------------- 1 | import FavoriteOulet from '../src/scripts/data/favorite-outlet'; 2 | import * as TestFactory from './helpers/testFactory'; 3 | 4 | const addLikeAction = () => { 5 | document.body.innerHTML = '
'; 6 | }; 7 | 8 | describe('Liking A Favorite Outlet Category', () => { 9 | // beforeEach make for running addLikeAction() every time when the test is run 10 | beforeEach(() => { 11 | addLikeAction(); 12 | }); 13 | 14 | it('should show the like outlet when the outlet has not been liked before', async () => { 15 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 16 | expect(document.querySelector('[aria-label="like outlet"]')).toBeTruthy(); 17 | }); 18 | 19 | it('should not show unlike outlet when outlet has not been liked before', async () => { 20 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 21 | expect(document.querySelector('[aria-label="unlike outlet"]')).toBeFalsy(); 22 | }); 23 | 24 | it('should be able to like oulet', async () => { 25 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 26 | document.querySelector('#likeButton').dispatchEvent(new Event('click')); 27 | const outlet = await FavoriteOulet.getOutlet(1); 28 | 29 | expect(outlet).toEqual({ id: 1 }); 30 | FavoriteOulet.deleteOutlet(1); 31 | }); 32 | 33 | it("should not add a outlet again when it's already liked", async () => { 34 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 35 | 36 | // get the outlet from favorite outlet 37 | await FavoriteOulet.putOutlet({ id: 1 }); 38 | document.querySelector('#likeButton').dispatchEvent(new Event('click')); 39 | 40 | expect(await FavoriteOulet.getAllOutlets()).toEqual([{ id: 1 }]); 41 | FavoriteOulet.deleteOutlet(1); 42 | }); 43 | 44 | it("should can't like oulet if oulet not have id", async () => { 45 | await TestFactory.createLikeButtonPresenterWithOutlet({}); 46 | 47 | document.querySelector('#likeButton').dispatchEvent(new Event('click')); 48 | expect(await FavoriteOulet.getAllOutlets()).toEqual([]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /specs/unlikeOuletSpec.js: -------------------------------------------------------------------------------- 1 | import FavoriteOulet from '../src/scripts/data/favorite-outlet'; 2 | import * as TestFactory from './helpers/testFactory'; 3 | 4 | describe('Unlike a Favorite Oulet Category', () => { 5 | const addLikeAction = () => { 6 | document.body.innerHTML = '
'; 7 | }; 8 | 9 | beforeEach(async () => { 10 | addLikeAction(); 11 | await FavoriteOulet.putOutlet({ id: 1 }); 12 | }); 13 | 14 | // afterEach make for running removeLikeAction() every time when the test is run have been completed 15 | afterEach(async () => { 16 | await FavoriteOulet.deleteOutlet(1); 17 | }); 18 | 19 | it('should display unlike widget when the outlet has been liked', async () => { 20 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 21 | 22 | expect(document.querySelector('[aria-label="unlike outlet"]')).toBeTruthy(); 23 | }); 24 | 25 | it('should not display like widget when the outlet has been liked', async () => { 26 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 27 | 28 | expect(document.querySelector('[aria-label="like outlet"]')).toBeFalsy(); 29 | }); 30 | 31 | it('should be able to remove liked outlet from the list', async () => { 32 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 33 | 34 | document 35 | .querySelector('[aria-label="unlike outlet"]') 36 | .dispatchEvent(new Event('click')); 37 | 38 | expect(await FavoriteOulet.getAllOutlets()).toEqual([]); 39 | }); 40 | 41 | it('should not throw error if the unliked outlet is not in the favorite oulet', async () => { 42 | await TestFactory.createLikeButtonPresenterWithOutlet({ id: 1 }); 43 | 44 | await FavoriteOulet.deleteOutlet(1); 45 | 46 | document 47 | .querySelector('[aria-label="unlike outlet"]') 48 | .dispatchEvent(new Event('click')); 49 | 50 | expect(await FavoriteOulet.getAllOutlets()).toEqual([]); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/public/icons-set/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/icons-set/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/icons-set/heart-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/icons-set/heart-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/icons-set/location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/icons-set/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/public/icons-set/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/icons-set/white-location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/public/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/public/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/public/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/public/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/public/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/public/images/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/404.jpg -------------------------------------------------------------------------------- /src/public/images/data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/data.jpg -------------------------------------------------------------------------------- /src/public/images/hero-large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/hero-large.jpg -------------------------------------------------------------------------------- /src/public/images/hero-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/hero-small.jpg -------------------------------------------------------------------------------- /src/public/images/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/hero.jpg -------------------------------------------------------------------------------- /src/public/images/page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/page.jpg -------------------------------------------------------------------------------- /src/public/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/placeholder.png -------------------------------------------------------------------------------- /src/public/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rolandowebdev/bites/f31fae36f337c8aa315f0d188b1e9f9f4fec99f5/src/public/images/profile.jpg -------------------------------------------------------------------------------- /src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bites", 3 | "short_name": "Bites", 4 | "description": "Bites is the website of a restaurant called Bites which is very famous in Indonesia ", 5 | "start_url": "/index.html", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#ff8303", 9 | "icons": [{ 10 | "src": "/icons/icon-72x72.png", 11 | "sizes": "72x72", 12 | "type": "image/png", 13 | "purpose": "any maskable" 14 | }, 15 | { 16 | "src": "/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png", 19 | "purpose": "any maskable" 20 | }, 21 | { 22 | "src": "/icons/icon-128x128.png", 23 | "sizes": "128x128", 24 | "type": "image/png", 25 | "purpose": "any maskable" 26 | }, 27 | { 28 | "src": "/icons/icon-144x144.png", 29 | "sizes": "144x144", 30 | "type": "image/png", 31 | "purpose": "any maskable" 32 | }, 33 | { 34 | "src": "/icons/icon-152x152.png", 35 | "sizes": "152x152", 36 | "type": "image/png", 37 | "purpose": "any maskable" 38 | }, 39 | { 40 | "src": "/icons/icon-192x192.png", 41 | "sizes": "192x192", 42 | "type": "image/png", 43 | "purpose": "any maskable" 44 | }, 45 | { 46 | "src": "/icons/icon-384x384.png", 47 | "sizes": "384x384", 48 | "type": "image/png", 49 | "purpose": "any maskable" 50 | }, 51 | { 52 | "src": "/icons/icon-512x512.png", 53 | "sizes": "512x512", 54 | "type": "image/png", 55 | "purpose": "any maskable" 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /src/scripts/components/AllFood.js: -------------------------------------------------------------------------------- 1 | import { skeletonFood } from '../views/templates/local-template'; 2 | 3 | class AllFood extends HTMLElement { 4 | connectedCallback() { 5 | this.render(); 6 | } 7 | 8 | render() { 9 | this.innerHTML = ` 10 |
11 |

All Food For You

12 |
13 | ${skeletonFood(6)} 14 |
15 |
16 | `; 17 | } 18 | } 19 | 20 | customElements.define('all-food', AllFood); 21 | -------------------------------------------------------------------------------- /src/scripts/components/Choose.js: -------------------------------------------------------------------------------- 1 | class Choose extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 |

10 | Why you choose Bites? 11 |

12 |
13 | 14 |
15 |
16 | `; 17 | } 18 | } 19 | 20 | customElements.define('choose-component', Choose); 21 | -------------------------------------------------------------------------------- /src/scripts/components/ExcessSection.js: -------------------------------------------------------------------------------- 1 | class Excess extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 |
10 |

01

11 |

Great Location

12 |

13 | Discover nature's best. Prime location, breathtaking views. 14 |

15 |
16 |
17 |

02

18 |

Natural Environment

19 |

Discover a natural oasis. Prime location, breathtaking views.

20 |
21 |
22 |

03

23 |

Healthy Food

24 |

Indulge in healthy dining. Prime location, breathtaking views.

25 |
26 |
27 | `; 28 | } 29 | } 30 | 31 | customElements.define('excess-component', Excess); 32 | -------------------------------------------------------------------------------- /src/scripts/components/FavoriteNotFound.js: -------------------------------------------------------------------------------- 1 | class FavoriteNotFound extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 | Not Found 10 |

11 | You haven't chosen your favorite outlet

12 |

13 | Please choose your favorite outlet on the outlet page

14 |
15 | `; 16 | } 17 | } 18 | 19 | customElements.define('favorite-notfound', FavoriteNotFound); 20 | -------------------------------------------------------------------------------- /src/scripts/components/Footer.js: -------------------------------------------------------------------------------- 1 | class Footer extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 | 43 | `; 44 | } 45 | } 46 | 47 | customElements.define('footer-component', Footer); 48 | -------------------------------------------------------------------------------- /src/scripts/components/Loading.js: -------------------------------------------------------------------------------- 1 | class Loading extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 |
10 |
11 | `; 12 | } 13 | } 14 | 15 | customElements.define('loading-component', Loading); 16 | -------------------------------------------------------------------------------- /src/scripts/components/Main.js: -------------------------------------------------------------------------------- 1 | class Main extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 |
10 | `; 11 | } 12 | } 13 | 14 | customElements.define('main-component', Main); 15 | -------------------------------------------------------------------------------- /src/scripts/components/MostFood.js: -------------------------------------------------------------------------------- 1 | import { skeletonFood } from '../views/templates/local-template'; 2 | 3 | class MostFood extends HTMLElement { 4 | connectedCallback() { 5 | this.render(); 6 | } 7 | 8 | render() { 9 | this.innerHTML = ` 10 |
11 |

Most Ordered Food

12 | 13 |
14 | ${skeletonFood(4)} 15 |
16 |
17 | `; 18 | } 19 | } 20 | 21 | customElements.define('most-food', MostFood); 22 | -------------------------------------------------------------------------------- /src/scripts/components/Navbar.js: -------------------------------------------------------------------------------- 1 | class Navbar extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 | 10 | 60 |
61 | 62 | 63 | Heroes Image 64 | 65 |

We Serve The Taste
You Love.

66 |

67 | This is a type of restaurant which typically serves food and drinks. in addition to light refreshments such as baked goods or scanks. The term comes the rench word meaning food. 68 |

69 |
70 |
71 | `; 72 | } 73 | } 74 | 75 | customElements.define('navbar-component', Navbar); 76 | -------------------------------------------------------------------------------- /src/scripts/components/Outlet.js: -------------------------------------------------------------------------------- 1 | import { createSkeletonUi } from '../views/templates/api-template'; 2 | 3 | class Outlet extends HTMLElement { 4 | connectedCallback() { 5 | this.render(); 6 | } 7 | 8 | render() { 9 | this.innerHTML = ` 10 |
11 |

Explore All Our Outlets

12 | 13 |
14 | ${createSkeletonUi(20)} 15 |
16 |
17 | `; 18 | } 19 | } 20 | 21 | customElements.define('outlet-component', Outlet); 22 | -------------------------------------------------------------------------------- /src/scripts/components/detail-component/ButtonContainer.js: -------------------------------------------------------------------------------- 1 | class ButtonContainer extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 | `; 10 | } 11 | } 12 | 13 | customElements.define('button-container', ButtonContainer); 14 | -------------------------------------------------------------------------------- /src/scripts/components/detail-component/DetailOutlet.js: -------------------------------------------------------------------------------- 1 | class DetailOutlet extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 | `; 10 | } 11 | } 12 | 13 | customElements.define('detail-outlet', DetailOutlet); 14 | -------------------------------------------------------------------------------- /src/scripts/components/detail-component/FormContainer.js: -------------------------------------------------------------------------------- 1 | class FormContainer extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 |
10 | 11 | 19 | 22 |
23 | 24 | 25 | 26 |
27 | `; 28 | } 29 | } 30 | 31 | customElements.define('form-container', FormContainer); 32 | -------------------------------------------------------------------------------- /src/scripts/components/detail-component/Menu.js: -------------------------------------------------------------------------------- 1 | class MenuContainer extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 | 21 | `; 22 | } 23 | } 24 | 25 | customElements.define('menu-container', MenuContainer); 26 | -------------------------------------------------------------------------------- /src/scripts/components/detail-component/NotFound.js: -------------------------------------------------------------------------------- 1 | class NotFound extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 | Not Found 10 |

404 Not Found

11 |

Failed to fetch data, please check your connection

12 |
13 | `; 14 | } 15 | } 16 | 17 | customElements.define('not-found', NotFound); 18 | -------------------------------------------------------------------------------- /src/scripts/components/detail-component/Review.js: -------------------------------------------------------------------------------- 1 | class Review extends HTMLElement { 2 | connectedCallback() { 3 | this.render(); 4 | } 5 | 6 | render() { 7 | this.innerHTML = ` 8 |
9 |

Customer Reviews

10 |
11 |
12 | `; 13 | } 14 | } 15 | 16 | customElements.define('review-component', Review); 17 | -------------------------------------------------------------------------------- /src/scripts/data/data-outlet.js: -------------------------------------------------------------------------------- 1 | import API_ENDPOINT from '../global/api-endpoint'; 2 | 3 | class SourceOutlet { 4 | static async allOutlet() { 5 | const response = await fetch(API_ENDPOINT.LIST_OUTLET); 6 | const responseJson = await response.json(); 7 | return responseJson; 8 | } 9 | 10 | static async detailOutlet(id) { 11 | const response = await fetch(API_ENDPOINT.DETAIL_OUTLET(id)); 12 | return response.json(); 13 | } 14 | 15 | static async postReview(review) { 16 | const response = await fetch(API_ENDPOINT.REVIEW_OUTLET, { 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify(review), 22 | }); 23 | return response; 24 | } 25 | } 26 | 27 | export default SourceOutlet; 28 | -------------------------------------------------------------------------------- /src/scripts/data/favorite-outlet.js: -------------------------------------------------------------------------------- 1 | import { openDB } from 'idb'; 2 | import CONFIG from '../global/config'; 3 | 4 | const { DATABASE_NAME, DATABASE_VERSION, OBJECT_STORE_NAME } = CONFIG; 5 | 6 | const databasePromise = openDB(DATABASE_NAME, DATABASE_VERSION, { 7 | upgrade(database) { 8 | database.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id' }); 9 | }, 10 | }); 11 | 12 | const FavoriteOutletDatabase = { 13 | async getOutlet(id) { 14 | if (!id) return; 15 | return (await databasePromise).get(OBJECT_STORE_NAME, id); 16 | }, 17 | 18 | async getAllOutlets() { 19 | return (await databasePromise).getAll(OBJECT_STORE_NAME); 20 | }, 21 | 22 | async putOutlet(outlet) { 23 | if (!outlet.hasOwnProperty('id')) return; 24 | return (await databasePromise).put(OBJECT_STORE_NAME, outlet); 25 | }, 26 | 27 | async deleteOutlet(id) { 28 | return (await databasePromise).delete(OBJECT_STORE_NAME, id); 29 | }, 30 | 31 | async searchOutlet(query) { 32 | return (await this.getAllOutlets()).filter((outlet) => { 33 | const loweredCaseOutletName = (outlet.name || '-').toLowerCase(); 34 | const OutletName = loweredCaseOutletName.replace(/\s/g, ''); 35 | 36 | const loweredCaseQuery = query.toLowerCase(); 37 | const Query = loweredCaseQuery.replace(/\s/g, ''); 38 | 39 | return OutletName.indexOf(Query) !== -1; 40 | }); 41 | }, 42 | }; 43 | 44 | export default FavoriteOutletDatabase; 45 | -------------------------------------------------------------------------------- /src/scripts/global/api-endpoint.js: -------------------------------------------------------------------------------- 1 | import CONFIG from './config'; 2 | 3 | const API_ENDPOINT = { 4 | LIST_OUTLET: `${CONFIG.BASE_URL}/list`, 5 | REVIEW_OUTLET: `${CONFIG.BASE_URL}/review`, 6 | DETAIL_OUTLET: (id) => `${CONFIG.BASE_URL}/detail/${id}`, 7 | }; 8 | 9 | export default API_ENDPOINT; 10 | -------------------------------------------------------------------------------- /src/scripts/global/config.js: -------------------------------------------------------------------------------- 1 | const CONFIG = { 2 | BASE_URL: 'https://restaurant-api.dicoding.dev', 3 | BASE_IMAGE_URL: 'https://restaurant-api.dicoding.dev/images/small/', 4 | CACHE_NAME: 'outlet', 5 | DATABASE_NAME: 'bites-database', 6 | DATABASE_VERSION: 1, 7 | OBJECT_STORE_NAME: 'outlets', 8 | }; 9 | 10 | export default CONFIG; 11 | -------------------------------------------------------------------------------- /src/scripts/index.js: -------------------------------------------------------------------------------- 1 | import 'lazysizes'; 2 | import 'regenerator-runtime/runtime'; 3 | import 'lazysizes/plugins/parent-fit/ls.parent-fit'; 4 | import './components/Footer'; 5 | import './components/Navbar'; 6 | import './components/Main'; 7 | import './components/AllFood'; 8 | import './components/MostFood'; 9 | import './components/Choose'; 10 | import './components/Outlet'; 11 | import './components/FavoriteNotFound'; 12 | import './components/Loading'; 13 | import './components/ExcessSection'; 14 | import './components/detail-component/NotFound'; 15 | import './components/detail-component/Menu'; 16 | import './components/detail-component/DetailOutlet'; 17 | import './components/detail-component/Review'; 18 | import './components/detail-component/ButtonContainer'; 19 | import './components/detail-component/FormContainer'; 20 | import '../styles/style.css'; 21 | import '../styles/responsive.css'; 22 | import '../styles/detail.css'; 23 | import '../styles/loading.css'; 24 | import '../styles/skeleton.css'; 25 | import '../public/images/hero.jpg'; 26 | import './views/pages/home'; 27 | import hideDrawer from './utils/close-drawer'; 28 | import swRegister from './utils/sw-register'; 29 | import App from './views/app'; 30 | 31 | const menu = document.querySelectorAll('.list-items'); 32 | const drawer = document.querySelector('#navbar .nav-list'); 33 | const checkbox = document.querySelector('.hamburger-menu input'); 34 | const hamburger = document.querySelectorAll('.hamburger-menu span'); 35 | const nav = document.querySelector('#navbar'); 36 | 37 | const app = new App({ 38 | button: document.querySelector('#hamburger'), 39 | drawer: document.querySelector('#navbar .nav-list'), 40 | content: document.querySelector('#mainContent'), 41 | }); 42 | 43 | window.addEventListener('hashchange', () => { 44 | app.renderPage(); 45 | }); 46 | 47 | window.addEventListener('load', () => { 48 | app.renderPage(); 49 | swRegister(); 50 | }); 51 | 52 | window.addEventListener('scroll', (event) => { 53 | event.stopPropagation(); 54 | if (document.documentElement.scrollTop || document.body.scrollTop) { 55 | nav.classList.add('nav-colored'); 56 | nav.classList.remove('nav-transparent'); 57 | } else { 58 | nav.classList.add('nav-transparent'); 59 | nav.classList.remove('nav-colored'); 60 | } 61 | }); 62 | 63 | hideDrawer(menu, drawer, checkbox, hamburger); 64 | -------------------------------------------------------------------------------- /src/scripts/json/BITES.json: -------------------------------------------------------------------------------- 1 | { 2 | "most": [{ 3 | "id": "1", 4 | "name": "Fish Teriaki", 5 | "location": "Western Food", 6 | "price": 55, 7 | "pictureId": "https://images.unsplash.com/photo-1476224203421-9ac39bcb3327?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1470&q=80", 8 | "badge": "Most Favorite" 9 | }, 10 | { 11 | "id": "2", 12 | "name": "Steak Wagyu", 13 | "location": "American Food", 14 | "price": 55, 15 | "pictureId": "https://images.unsplash.com/photo-1504674900247-0877df9cc836?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80", 16 | "badge": "Most Favorite" 17 | }, 18 | { 19 | "id": "3", 20 | "name": "Red Curry", 21 | "location": "American Food", 22 | "price": 55, 23 | "pictureId": "https://images.unsplash.com/photo-1455619452474-d2be8b1e70cd?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80", 24 | "badge": "Most Favorite" 25 | }, 26 | { 27 | "id": "4", 28 | "name": "Pork Chop Dinner", 29 | "location": "American Food", 30 | "price": 55, 31 | "pictureId": "https://images.unsplash.com/photo-1432139555190-58524dae6a55?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1476&q=80", 32 | "badge": "Most Favorite" 33 | }, 34 | { 35 | "id": "5", 36 | "name": "Pepperoni Pizza", 37 | "location": "Western Food", 38 | "price": 55, 39 | "pictureId": "https://images.unsplash.com/photo-1458642849426-cfb724f15ef7?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80", 40 | "badge": "Most Favorite" 41 | }, 42 | { 43 | "id": "6", 44 | "name": "Cookies Fruit", 45 | "location": "Western Food", 46 | "price": 55, 47 | "pictureId": "https://images.unsplash.com/photo-1496412705862-e0088f16f791?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80", 48 | "badge": "Most Favorite" 49 | }, 50 | { 51 | "id": "7", 52 | "name": "Sate Kambing", 53 | "location": "Indonesian Food", 54 | "price": 40, 55 | "pictureId": "https://images.unsplash.com/photo-1520690594286-1aadf7e7af36?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80", 56 | "badge": "Most Favorite" 57 | } 58 | ], 59 | "all": [{ 60 | "id": "1", 61 | "name": "Seafood Pasta", 62 | "location": "Western Food", 63 | "price": 38, 64 | "pictureId": "https://images.unsplash.com/photo-1563379926898-05f4575a45d8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80" 65 | }, 66 | { 67 | "id": "2", 68 | "name": "Seafood Paella", 69 | "location": "Western Food", 70 | "price": 28, 71 | "pictureId": "https://images.unsplash.com/photo-1515443961218-a51367888e4b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80" 72 | }, 73 | { 74 | "id": "3", 75 | "name": "Lontong Sayur", 76 | "location": "Indonesian Food", 77 | "price": 20, 78 | "pictureId": "https://images.unsplash.com/photo-1572656306390-40a9fc3899f7?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80" 79 | }, 80 | { 81 | "id": "4", 82 | "name": "Bowl Of Oatmeal", 83 | "location": "Western Food", 84 | "price": 20, 85 | "pictureId": "https://images.unsplash.com/photo-1497888329096-51c27beff665?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=871&q=80" 86 | }, 87 | { 88 | "id": "5", 89 | "name": "Udang Sauce Tiram", 90 | "location": "Indonesian Food", 91 | "price": 20, 92 | "pictureId": "https://images.unsplash.com/photo-1626508034913-8648f3f84b84?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80" 93 | }, 94 | { 95 | "id": "6", 96 | "name": "Yam Mie", 97 | "location": "Chinese Food", 98 | "price": 20, 99 | "pictureId": "https://images.unsplash.com/photo-1626509653291-18d9a934b9db?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80" 100 | } 101 | ], 102 | "choose": [{ 103 | "id": "1", 104 | "title": "A Very Simple Process To Make Order Your Favorite Foods", 105 | "reasonOne": "We serve authentic food from the ground up", 106 | "reasonTwo": "A special and appetizing presentation", 107 | "reasonThree": "Unique quality of food and presentation", 108 | "reasonFour": "The best way for get your favorite food", 109 | "pictureId": "https://images.unsplash.com/photo-1592861956120-e524fc739696?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=870&q=80" 110 | }] 111 | } -------------------------------------------------------------------------------- /src/scripts/routes/routes.js: -------------------------------------------------------------------------------- 1 | import Outlet from '../views/pages/outlet'; 2 | import DetailOutlets from '../views/pages/detail'; 3 | import Home from '../views/pages/home'; 4 | import Food from '../views/pages/food'; 5 | import Favorite from '../views/pages/favorite'; 6 | 7 | const routes = { 8 | '/': Home, 9 | '/food': Food, 10 | '/outlet': Outlet, 11 | '/detail/:id': DetailOutlets, 12 | '/favorite': Favorite, 13 | }; 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /src/scripts/routes/url-parser.js: -------------------------------------------------------------------------------- 1 | const UrlParser = { 2 | parseActiveUrlWithCombiner() { 3 | const url = window.location.hash.slice(1).toLowerCase(); 4 | const splitedUrl = this._urlSplitter(url); 5 | const scrollTop = this._scrollOnTop(); 6 | return this._urlCombiner(splitedUrl, scrollTop); 7 | }, 8 | 9 | parseActiveUrlWithoutCombiner() { 10 | const url = window.location.hash.slice(1).toLowerCase(); 11 | const scrollTop = this._scrollOnTop(); 12 | return this._urlSplitter(url, scrollTop); 13 | }, 14 | 15 | _scrollOnTop() { 16 | window.top.scrollTo(0, 0); 17 | }, 18 | 19 | _urlSplitter(url) { 20 | const urlsSplits = url.split('/'); 21 | return { 22 | resource: urlsSplits[1] || null, 23 | id: urlsSplits[2] || null, 24 | verb: urlsSplits[3] || null, 25 | }; 26 | }, 27 | 28 | _urlCombiner(splitedUrl) { 29 | return ( 30 | (splitedUrl.resource ? `/${splitedUrl.resource}` : '/') + 31 | (splitedUrl.id ? '/:id' : '') + 32 | (splitedUrl.verb ? `/${splitedUrl.verb}` : '') 33 | ); 34 | }, 35 | }; 36 | 37 | export default UrlParser; 38 | -------------------------------------------------------------------------------- /src/scripts/sw.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import CONFIG from './global/config'; 3 | import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute'; 4 | import { cleanupOutdatedCaches } from 'workbox-precaching'; 5 | import { registerRoute } from 'workbox-routing/registerRoute'; 6 | import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'; 7 | import { ExpirationPlugin } from 'workbox-expiration'; 8 | import { clientsClaim, setCacheNameDetails } from 'workbox-core'; 9 | 10 | setCacheNameDetails({ 11 | prefix: CONFIG.CACHE_NAME, 12 | precache: 'precache', 13 | runtime: 'runtime', 14 | }); 15 | 16 | precacheAndRoute( 17 | [ 18 | ...self.__WB_MANIFEST, 19 | { 20 | url: 'https://fonts.googleapis.com/css2?family=Croissant+One&family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap', 21 | revision: 1, 22 | }, 23 | { 24 | url: 'https://pro.fontawesome.com/releases/v5.10.0/css/all.css', 25 | revision: 1, 26 | }, 27 | ], 28 | { 29 | ignoreURLParametersMatching: [/.*/], 30 | } 31 | ); 32 | registerRoute( 33 | /^https:\/\/restaurant-api\.dicoding\.dev\/(?:(list|detail|review|search))/, 34 | new StaleWhileRevalidate({ 35 | cacheName: 'restaurant-data', 36 | plugins: [ 37 | new ExpirationPlugin({ 38 | maxEntries: 100, 39 | maxAgeSeconds: 30 * 24 * 60 * 60, 40 | }), 41 | ], 42 | }) 43 | ); 44 | 45 | registerRoute( 46 | ({ request }) => request.destination === 'image', 47 | new CacheFirst({ 48 | cacheName: 'image-data', 49 | plugins: [ 50 | new ExpirationPlugin({ 51 | maxEntries: 60, 52 | maxAgeSeconds: 30 * 24 * 60 * 60, 53 | }), 54 | ], 55 | }) 56 | ); 57 | 58 | self.skipWaiting(); 59 | clientsClaim(); 60 | 61 | cleanupOutdatedCaches(); 62 | -------------------------------------------------------------------------------- /src/scripts/utils/arrow-animation.js: -------------------------------------------------------------------------------- 1 | function arrowAnimation(link, arrows) { 2 | const arrow = arrows; 3 | link.addEventListener('mouseover', (event) => { 4 | event.stopPropagation(); 5 | arrow.style.transform = 'translateX(5px)'; 6 | }); 7 | 8 | link.addEventListener('mouseout', (event) => { 9 | event.stopPropagation(); 10 | arrow.style.transform = 'translateX(0px)'; 11 | }); 12 | } 13 | 14 | export default arrowAnimation; 15 | -------------------------------------------------------------------------------- /src/scripts/utils/check-online.js: -------------------------------------------------------------------------------- 1 | import Swal from 'sweetalert2'; 2 | 3 | const onlineStatus = window.navigator.onLine; 4 | const checkOnline = { 5 | status: () => { 6 | if (onlineStatus === false) { 7 | return Swal.fire({ 8 | title: 'Your connection offline', 9 | text: 'Please check your connection😪', 10 | icon: 'warning', 11 | }); 12 | } 13 | return false; 14 | }, 15 | }; 16 | 17 | export default checkOnline; 18 | -------------------------------------------------------------------------------- /src/scripts/utils/close-drawer.js: -------------------------------------------------------------------------------- 1 | function hideDrawer(menu, drawer, checkbox, hamburger) { 2 | const check = checkbox; 3 | menu.forEach((nav) => { 4 | nav.addEventListener('click', (event) => { 5 | event.stopPropagation(); 6 | window.scrollTo(0, 0); 7 | check.checked = false; 8 | drawer.classList.remove('slide'); 9 | if (check.checked === false) { 10 | hamburger.forEach((menus) => { 11 | const menu = menus; 12 | menu.style.backgroundColor = '#ff4f03'; 13 | }); 14 | } 15 | }); 16 | }); 17 | } 18 | 19 | export default hideDrawer; 20 | -------------------------------------------------------------------------------- /src/scripts/utils/drawer-initiator.js: -------------------------------------------------------------------------------- 1 | const DrawerInitiator = { 2 | init({ button, drawer, content }) { 3 | button.addEventListener('click', (event) => { 4 | this._toggleDrawer(event, drawer); 5 | }); 6 | 7 | content.addEventListener('click', (event) => { 8 | this._closeDrawer(event, drawer); 9 | }); 10 | }, 11 | 12 | _toggleDrawer(event, drawer) { 13 | event.stopPropagation(); 14 | drawer.classList.toggle('slide'); 15 | }, 16 | 17 | _closeDrawer(event, drawer) { 18 | event.stopPropagation(); 19 | drawer.classList.remove('slide'); 20 | }, 21 | }; 22 | 23 | export default DrawerInitiator; 24 | -------------------------------------------------------------------------------- /src/scripts/utils/form-input-handler.js: -------------------------------------------------------------------------------- 1 | import countIteration from './form-validation'; 2 | 3 | function formInputHandler(input, countInfo, iteration) { 4 | document.getElementById(input).addEventListener('input', () => { 5 | const jumlahKarakterDiketik = document.getElementById(input).value.length; 6 | const jumlahKarakterMaksimal = document.getElementById(input).maxLength; 7 | const sisaKarakterUpdate = jumlahKarakterMaksimal - jumlahKarakterDiketik; 8 | 9 | document.getElementById(iteration).innerText = sisaKarakterUpdate; 10 | countIteration(sisaKarakterUpdate, iteration, countInfo); 11 | }); 12 | 13 | document.getElementById(input).addEventListener('focus', () => { 14 | document.getElementById(countInfo).style.visibility = 'inherit'; 15 | }); 16 | 17 | document.getElementById(input).addEventListener('blur', () => { 18 | document.getElementById(countInfo).style.visibility = 'hidden'; 19 | }); 20 | } 21 | 22 | export default formInputHandler; 23 | -------------------------------------------------------------------------------- /src/scripts/utils/form-validation.js: -------------------------------------------------------------------------------- 1 | import Swal from 'sweetalert2/'; 2 | import SourceOutlet from '../data/data-outlet'; 3 | 4 | function countIteration(count, iterator, notif) { 5 | if (count === 0) { 6 | document.getElementById(iterator).innerText = 'max'; 7 | } else if (count <= 5) { 8 | document.getElementById(notif).style.color = 'red'; 9 | document.getElementById(iterator).style.color = 'red'; 10 | } else { 11 | document.getElementById(notif).style.color = '#AAA492'; 12 | document.getElementById(iterator).style.color = '#AAA492'; 13 | } 14 | } 15 | 16 | function handleInputFill(review) { 17 | if (review.name === '' || review.review === '') { 18 | Swal.fire({ 19 | title: 'All data must be filled!', 20 | text: 'Failed to send review feedback😒', 21 | }); 22 | } else { 23 | Swal.fire({ 24 | title: 'Successfully added review', 25 | text: 'Thank you for your feedback😄', 26 | }); 27 | SourceOutlet.postReview(review); 28 | } 29 | } 30 | 31 | export { countIteration, handleInputFill }; 32 | -------------------------------------------------------------------------------- /src/scripts/utils/hamburger-action.js: -------------------------------------------------------------------------------- 1 | function hamburgerAction(checkbox, hamburger) { 2 | document; 3 | checkbox.addEventListener('change', (event) => { 4 | event.stopPropagation(); 5 | hamburger.forEach((menus) => { 6 | const menu = menus; 7 | checkbox.checked === true 8 | ? (menu.style.backgroundColor = '#fff') 9 | : (menu.style.backgroundColor = '#ff4f03'); 10 | }); 11 | }); 12 | } 13 | 14 | export default hamburgerAction; 15 | -------------------------------------------------------------------------------- /src/scripts/utils/like-button-presenter.js: -------------------------------------------------------------------------------- 1 | import Swal from 'sweetalert2'; 2 | import { 3 | createLikeOuletTemplate, 4 | createUnlikeOuletTemplate, 5 | } from '../views/templates/api-template'; 6 | import checkOnline from './check-online'; 7 | 8 | const LikeButtonInitiator = { 9 | async init({ likeButtonContainer, favoriteOutlet, outlet }) { 10 | this._likeButtonContainer = likeButtonContainer; 11 | this._outlet = outlet; 12 | this._favoriteOutlet = favoriteOutlet; 13 | 14 | await this._renderButton(); 15 | }, 16 | 17 | async _renderButton() { 18 | const { id } = this._outlet; 19 | if (await this._isOutletExist(id)) { 20 | this._renderLiked(); 21 | } else { 22 | this._renderLike(); 23 | } 24 | }, 25 | 26 | async _isOutletExist(id) { 27 | const outlet = await this._favoriteOutlet.getOutlet(id); 28 | return !!outlet; 29 | }, 30 | 31 | _renderLike() { 32 | this._likeButtonContainer.innerHTML = createLikeOuletTemplate(); 33 | 34 | const likeButton = document.querySelector('#likeButton'); 35 | likeButton.addEventListener('click', async () => { 36 | if (window.navigator.onLine === true) { 37 | await this._favoriteOutlet.putOutlet(this._outlet); 38 | this._renderButton(); 39 | Swal.fire({ 40 | title: 'You have favorite outlet now!', 41 | text: 'Enjoy for all menu here😍', 42 | confirmButtonText: 43 | 'Let\'s go see all menu', 44 | }); 45 | } 46 | checkOnline.status(); 47 | }); 48 | }, 49 | 50 | _renderLiked() { 51 | this._likeButtonContainer.innerHTML = createUnlikeOuletTemplate(); 52 | 53 | const likeButton = document.querySelector('#likeButton'); 54 | likeButton.addEventListener('click', async () => { 55 | if (window.navigator.onLine === true) { 56 | await this._favoriteOutlet.deleteOutlet(this._outlet.id); 57 | this._renderButton(); 58 | Swal.fire({ 59 | title: 'Outlet has been removed!', 60 | text: 'Please come back later😥', 61 | }); 62 | } 63 | checkOnline.status(); 64 | }); 65 | }, 66 | }; 67 | 68 | export default LikeButtonInitiator; 69 | -------------------------------------------------------------------------------- /src/scripts/utils/loading-initiator.js: -------------------------------------------------------------------------------- 1 | const loading = { 2 | show() { 3 | document.getElementById('loading').style.display = 'block'; 4 | }, 5 | hide() { 6 | const fadeEffect = setInterval(() => { 7 | if (!document.getElementById('loading').style.opacity) { 8 | document.getElementById('loading').style.opacity = 1; 9 | } 10 | if (document.getElementById('loading').style.opacity > 0) { 11 | document.getElementById('loading').style.opacity -= 0.1; 12 | } else { 13 | clearInterval(fadeEffect); 14 | document.getElementById('loading').style.display = 'none'; 15 | } 16 | }); 17 | }, 18 | }; 19 | 20 | export default loading; 21 | -------------------------------------------------------------------------------- /src/scripts/utils/sw-register.js: -------------------------------------------------------------------------------- 1 | import { Workbox } from 'workbox-window'; 2 | 3 | const swRegister = async () => { 4 | if ('serviceWorker' in navigator) { 5 | const workbox = new Workbox('../sw.js'); 6 | workbox.register(); 7 | } 8 | }; 9 | 10 | export default swRegister; 11 | -------------------------------------------------------------------------------- /src/scripts/views/app.js: -------------------------------------------------------------------------------- 1 | import DrawerInitiator from '../utils/drawer-initiator'; 2 | import UrlParser from '../routes/url-parser'; 3 | import routes from '../routes/routes'; 4 | 5 | class App { 6 | constructor({ button, drawer, content }) { 7 | this._button = button; 8 | this._drawer = drawer; 9 | this._content = content; 10 | 11 | this._initialAppShell(); 12 | } 13 | 14 | _initialAppShell() { 15 | DrawerInitiator.init({ 16 | button: this._button, 17 | drawer: this._drawer, 18 | content: this._content, 19 | }); 20 | } 21 | 22 | async renderPage() { 23 | const url = UrlParser.parseActiveUrlWithCombiner(); 24 | const skipLink = document.querySelector('.skip-link'); 25 | const page = routes[url]; 26 | try { 27 | this._content.innerHTML = await page.render(); 28 | await page.afterRender(); 29 | skipLink.addEventListener('click', (event) => { 30 | event.preventDefault(); 31 | document.querySelector('#mainContent').focus(); 32 | }); 33 | } catch (error) { 34 | document.body.innerHTML = ` 35 |
36 | Page Not Found 37 | Go to the home 38 |

Your route is undefined, please back to the home

39 |
`; 40 | } 41 | } 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/scripts/views/pages/detail.js: -------------------------------------------------------------------------------- 1 | import UrlParser from '../../routes/url-parser'; 2 | import SourceOutlet from '../../data/data-outlet'; 3 | import FavoriteOutletDatabase from '../../data/favorite-outlet'; 4 | import checkOnline from '../../utils/check-online'; 5 | import LikeButtonPresenter from '../../utils/like-button-presenter'; 6 | import { countIteration, handleInputFill } from '../../utils/form-validation'; 7 | import { 8 | detailOutlet, 9 | foodMenu, 10 | drinkMenu, 11 | reviewOutlet, 12 | } from '../templates/api-template'; 13 | 14 | const DetailOutlets = { 15 | async render() { 16 | return ` 17 | 18 | 19 | 20 | 21 | 22 | 23 | `; 24 | }, 25 | 26 | async afterRender() { 27 | const detailOutletContainer = document.querySelector('#detail-outlet'); 28 | const foodContainer = document.querySelector('#foods'); 29 | const drinkContainer = document.querySelector('#drinks'); 30 | const reviewContainer = document.querySelector('#review'); 31 | 32 | const hero = document.querySelector('#hero'); 33 | const inputName = document.querySelector('#reviewName'); 34 | const inputReview = document.querySelector('#reviewDetail'); 35 | const submit = document.querySelector('#submit'); 36 | const iteration = document.querySelector('#countIteration'); 37 | const countInfo = document.querySelector('#countInfo'); 38 | 39 | try { 40 | const url = UrlParser.parseActiveUrlWithoutCombiner(); 41 | const outlet = await SourceOutlet.detailOutlet(url.id); 42 | 43 | hero.style.display = 'none'; 44 | 45 | inputName.addEventListener('input', () => { 46 | const characterTyped = inputName.value.length; 47 | const maxCharacter = inputName.maxLength; 48 | const remainingCharacter = maxCharacter - characterTyped; 49 | 50 | iteration.innerText = remainingCharacter; 51 | countIteration(remainingCharacter, 'countIteration', 'countInfo'); 52 | }); 53 | 54 | inputName.addEventListener('focus', () => { 55 | countInfo.style.visibility = 'initial'; 56 | }); 57 | 58 | inputName.addEventListener('blur', () => { 59 | countInfo.style.visibility = 'hidden'; 60 | }); 61 | 62 | submit.addEventListener('click', () => { 63 | const review = { 64 | id: url.id, 65 | name: inputName.value, 66 | review: inputReview.value, 67 | }; 68 | window.navigator.onLine === true 69 | ? handleInputFill(review) 70 | : checkOnline.status(); 71 | }); 72 | 73 | detailOutletContainer.innerHTML = detailOutlet(outlet.restaurant); 74 | 75 | outlet.restaurant.menus.foods.slice(0, 4).map((food) => { 76 | foodContainer.innerHTML += foodMenu(food); 77 | }); 78 | 79 | outlet.restaurant.menus.drinks.slice(0, 4).map((drink) => { 80 | drinkContainer.innerHTML += drinkMenu(drink); 81 | }); 82 | 83 | outlet.restaurant.customerReviews.map( 84 | (review) => (reviewContainer.innerHTML += reviewOutlet(review)) 85 | ); 86 | 87 | LikeButtonPresenter.init({ 88 | likeButtonContainer: document.querySelector('#likeButtonContainer'), 89 | favoriteOutlet: FavoriteOutletDatabase, 90 | outlet: { 91 | id: outlet.restaurant.id, 92 | name: outlet.restaurant.name, 93 | pictureId: outlet.restaurant.pictureId, 94 | description: outlet.restaurant.description, 95 | rating: outlet.restaurant.rating, 96 | city: outlet.restaurant.city, 97 | }, 98 | }); 99 | } catch (err) { 100 | document.querySelector('#notfound').style.display = 'block'; 101 | document.querySelector('.list-menu-description').style.display = 'none'; 102 | document.querySelector('#form-container').style.display = 'none'; 103 | document.querySelector('#reviews').style.display = 'none'; 104 | hero.style.display = 'none'; 105 | foodContainer.style.display = 'none'; 106 | drinkContainer.style.display = 'none'; 107 | reviewContainer.style.display = 'none'; 108 | detailOutletContainer.style.display = 'none'; 109 | } 110 | }, 111 | }; 112 | 113 | export default DetailOutlets; 114 | -------------------------------------------------------------------------------- /src/scripts/views/pages/favorite-outlet/favorite-outlet-search-presenter.js: -------------------------------------------------------------------------------- 1 | class FavoriteOutletSearchPresenter { 2 | constructor({ favoriteOutlet, view }) { 3 | this._view = view; 4 | this._listenToSearchRequestByUser(); 5 | this._favoriteOutlet = favoriteOutlet; 6 | } 7 | 8 | _listenToSearchRequestByUser() { 9 | this._view.runWhenUserIsSearching((latestQuery) => { 10 | this._searchOutlet(latestQuery); 11 | }); 12 | } 13 | 14 | async _searchOutlet(latestQuery) { 15 | this._latestQuery = latestQuery.trim(); 16 | 17 | let foundOutlet; 18 | 19 | if (this.latestQuery.length > 0) { 20 | foundOutlet = await this._favoriteOutlet.searchOutlet(this.latestQuery); 21 | } else { 22 | foundOutlet = await this._favoriteOutlet.getAllOutlets(); 23 | } 24 | 25 | this._showFoundOutlet(foundOutlet); 26 | } 27 | 28 | _showFoundOutlet(outlet) { 29 | this._view.showFavoriteOutlet(outlet); 30 | } 31 | 32 | get latestQuery() { 33 | return this._latestQuery; 34 | } 35 | } 36 | 37 | export default FavoriteOutletSearchPresenter; 38 | -------------------------------------------------------------------------------- /src/scripts/views/pages/favorite-outlet/favorite-outlet-search-view.js: -------------------------------------------------------------------------------- 1 | import { listOutlet } from '../../templates/api-template'; 2 | 3 | class FavoriteOutletSearchView { 4 | getTemplate() { 5 | return ` 6 |
7 |

Your Favorite Outlets Here

8 |
9 | 10 | 11 |
12 | 13 |
14 |
15 | `; 16 | } 17 | 18 | runWhenUserIsSearching(callback) { 19 | document.getElementById('query').addEventListener('change', (event) => { 20 | callback(event.target.value); 21 | }); 22 | } 23 | 24 | showOutlet(outlet) { 25 | this.showFavoriteOutlet(outlet); 26 | } 27 | 28 | showFavoriteOutlet(outlet = []) { 29 | let html; 30 | 31 | if (outlet.length) { 32 | html = outlet.reduce( 33 | (carry, outlet) => carry.concat(listOutlet(outlet)), 34 | '' 35 | ); 36 | } else { 37 | html = this._getEmptyOutletTemplate(); 38 | } 39 | 40 | document.querySelector('#outlet').innerHTML = html; 41 | 42 | if (outlet.length === 2) { 43 | document.querySelector('.outlet-container .outlet-card').style.maxWidth = 44 | '100%'; 45 | } 46 | 47 | document.querySelector('.outlet-container').style.marginTop = '0'; 48 | 49 | document 50 | .querySelector('#outlet') 51 | .dispatchEvent(new Event('outlet:updated')); 52 | } 53 | 54 | _getEmptyOutletTemplate() { 55 | return ` 56 |
57 | Not Found 58 |

The outlet you are looking for does not exist

59 |

60 | Search again and make sure your outlet name what you want 61 |

62 |
63 | `; 64 | } 65 | } 66 | 67 | export default FavoriteOutletSearchView; 68 | -------------------------------------------------------------------------------- /src/scripts/views/pages/favorite-outlet/favorite-outlet-show-presenter.js: -------------------------------------------------------------------------------- 1 | class FavoriteOutletShowPresenter { 2 | constructor({ view, favoriteOutlet }) { 3 | this._view = view; 4 | this._favoriteOutlet = favoriteOutlet; 5 | 6 | this._showFavoriteOutlet(); 7 | } 8 | 9 | async _showFavoriteOutlet() { 10 | const outlet = await this._favoriteOutlet.getAllOutlets(); 11 | this._displayOutlet(outlet); 12 | } 13 | 14 | _displayOutlet(outlet) { 15 | this._view.showFavoriteOutlet(outlet); 16 | } 17 | } 18 | 19 | export default FavoriteOutletShowPresenter; 20 | -------------------------------------------------------------------------------- /src/scripts/views/pages/favorite.js: -------------------------------------------------------------------------------- 1 | import FavoriteOutletDatabase from '../../data/favorite-outlet'; 2 | import FavoriteOutletSearchView from './favorite-outlet/favorite-outlet-search-view'; 3 | import FavoriteOutletShowPresenter from './favorite-outlet/favorite-outlet-show-presenter'; 4 | import FavoriteOutletSearchPresenter from './favorite-outlet/favorite-outlet-search-presenter'; 5 | import data from '../../json/BITES.json'; 6 | import { chooseBites } from '../templates/local-template'; 7 | 8 | const view = new FavoriteOutletSearchView(); 9 | 10 | const Favorite = { 11 | async render() { 12 | return ` 13 | ${view.getTemplate()} 14 | 15 | 16 | `; 17 | }, 18 | 19 | async afterRender() { 20 | const outlets = await FavoriteOutletDatabase.getAllOutlets(); 21 | const hero = document.querySelector('#hero'); 22 | let dataChoose = ''; 23 | 24 | hero.style.display = 'none'; 25 | 26 | data['choose'].map((data) => { 27 | dataChoose += chooseBites(data); 28 | }); 29 | 30 | new FavoriteOutletShowPresenter({ 31 | view, 32 | favoriteOutlet: FavoriteOutletDatabase, 33 | }); 34 | 35 | new FavoriteOutletSearchPresenter({ 36 | view, 37 | favoriteOutlet: FavoriteOutletDatabase, 38 | }); 39 | 40 | if (outlets.length === 0) { 41 | document.querySelector('#notfound-container').style.display = 'block'; 42 | document.querySelector('#outlet-section').style.display = 'none'; 43 | document.querySelector('choose-component').style.display = 'none'; 44 | } else { 45 | document.querySelector('#notfound-container').style.display = 'none'; 46 | document.querySelector('#outlet-section').style.display = 'block'; 47 | document.querySelector('#choose-section').style.display = 'block'; 48 | document.querySelector('#choose').innerHTML = dataChoose; 49 | } 50 | }, 51 | }; 52 | 53 | export default Favorite; 54 | -------------------------------------------------------------------------------- /src/scripts/views/pages/food.js: -------------------------------------------------------------------------------- 1 | import data from '../../json/BITES.json'; 2 | import { allFoodData, chooseBites } from '../templates/local-template'; 3 | 4 | const Food = { 5 | render() { 6 | return ` 7 | 8 | 9 | `; 10 | }, 11 | 12 | afterRender() { 13 | const allFoodContainer = document.querySelector('#all'); 14 | const hero = document.querySelector('#hero'); 15 | const allFood = data['all']; 16 | const choose = data['choose']; 17 | 18 | hero.style.display = 'none'; 19 | 20 | let dataAllFood = ''; 21 | let dataChoose = ''; 22 | allFoodContainer.innerHTML = ''; 23 | 24 | allFood.map((food) => { 25 | dataAllFood += allFoodData(food); 26 | }); 27 | 28 | choose.map((data) => { 29 | dataChoose += chooseBites(data); 30 | }); 31 | 32 | allFoodContainer.innerHTML = dataAllFood; 33 | document.querySelector('#choose').innerHTML = dataChoose; 34 | }, 35 | }; 36 | 37 | export default Food; 38 | -------------------------------------------------------------------------------- /src/scripts/views/pages/home.js: -------------------------------------------------------------------------------- 1 | import data from '../../json/BITES.json'; 2 | import SourceOutlet from '../../data/data-outlet'; 3 | import loading from '../../utils/loading-initiator'; 4 | import arrowAnimation from '../../utils/arrow-animation'; 5 | import hamburgerAction from '../../utils/hamburger-action'; 6 | import { listOutlet } from '../templates/api-template'; 7 | import { mostFoodData, chooseBites } from '../templates/local-template'; 8 | 9 | const Home = { 10 | async render() { 11 | return ` 12 | 13 | 14 | 15 | 16 | 17 | `; 18 | }, 19 | 20 | async afterRender() { 21 | loading.show(); 22 | const outlet = await SourceOutlet.allOutlet(); 23 | const mostFood = document.querySelector('#most'); 24 | const hamburger = document.querySelectorAll('.hamburger-menu span'); 25 | const checkbox = document.querySelector('.hamburger-menu input'); 26 | const linkOutlet = document.querySelector('.outlet-link'); 27 | const linkFood = document.querySelector('.food-link'); 28 | const arrowOutlet = document.querySelector('#fasOutlet'); 29 | const arrowFood = document.querySelector('#fasFood'); 30 | const outletWrapper = document.querySelector('#outlet'); 31 | const hero = document.querySelector('#hero'); 32 | 33 | let dataChoose = ''; 34 | let dataMostFood = ''; 35 | outletWrapper.innerHTML = ''; 36 | mostFood.innerHTML = ''; 37 | 38 | hero.style.display = 'block'; 39 | 40 | if (window.location.pathname === '/') { 41 | linkOutlet.style.display = 'block'; 42 | document.querySelector('.outlet-container').style.marginTop = '1rem'; 43 | } 44 | 45 | arrowAnimation(linkOutlet, arrowOutlet); 46 | arrowAnimation(linkFood, arrowFood); 47 | hamburgerAction(checkbox, hamburger); 48 | 49 | outlet.restaurants.slice(0, 6).map((outlet) => { 50 | outletWrapper.innerHTML += listOutlet(outlet); 51 | }); 52 | 53 | data['most'].slice(0, 6).map((data) => { 54 | dataMostFood += mostFoodData(data); 55 | }); 56 | 57 | data['choose'].map((data) => { 58 | dataChoose += chooseBites(data); 59 | }); 60 | 61 | mostFood.innerHTML = dataMostFood; 62 | document.querySelector('#choose').innerHTML = dataChoose; 63 | loading.hide(); 64 | }, 65 | }; 66 | 67 | export default Home; 68 | -------------------------------------------------------------------------------- /src/scripts/views/pages/outlet.js: -------------------------------------------------------------------------------- 1 | import SourceOutlet from '../../data/data-outlet'; 2 | import data from '../../json/BITES.json'; 3 | import loading from '../../utils/loading-initiator'; 4 | import { listOutlet } from '../templates/api-template'; 5 | import { chooseBites } from '../templates/local-template'; 6 | 7 | const Outlets = { 8 | async render() { 9 | return ` 10 | 11 | 12 | 13 | `; 14 | }, 15 | 16 | async afterRender() { 17 | loading.show(); 18 | const outlet = await SourceOutlet.allOutlet(); 19 | const outletWrapper = document.querySelector('#outlet'); 20 | const hero = document.querySelector('#hero'); 21 | outletWrapper.innerHTML = ''; 22 | 23 | hero.style.display = 'none'; 24 | 25 | outlet.restaurants.map((outlet) => { 26 | outletWrapper.innerHTML += listOutlet(outlet); 27 | }); 28 | 29 | const choose = data['choose']; 30 | let dataChoose = ''; 31 | 32 | choose.map((data) => { 33 | dataChoose += chooseBites(data); 34 | }); 35 | 36 | document.querySelector('#choose').innerHTML = dataChoose; 37 | loading.hide(); 38 | }, 39 | }; 40 | 41 | export default Outlets; 42 | -------------------------------------------------------------------------------- /src/scripts/views/templates/api-template.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../global/config'; 2 | 3 | const listOutlet = (outlet) => { 4 | return ` 5 |
6 | 9 |
10 | Lazyload Image 14 |
15 |
16 |
17 |

18 | 21 | ${outlet.name || 'outlet not found'} 22 | 23 |

24 |

Location Icon${ 25 | outlet.city || 'outlet not found' 26 | } 27 |

28 |

${outlet.description}

29 |

Star Logo 30 | ${outlet.rating || 'outlet not found'} 31 |

32 |
33 |
34 | `; 35 | }; 36 | 37 | const createSkeletonUi = (count) => { 38 | let skeleton = ''; 39 | for (let i = 0; i < count; i += 1) { 40 | skeleton += ` 41 |
42 |
43 | Lazyload Image 45 |
46 |
47 |

48 |

49 |

50 |

51 |
52 |
53 | `; 54 | } 55 | return skeleton; 56 | }; 57 | 58 | const detailOutlet = (outlet) => { 59 | return ` 60 |
61 |

${outlet.name}

62 |

${outlet.address} ${outlet.city}

63 |
64 |
65 | ${outlet.name} 68 |
69 |
70 |

About ${outlet.name}

71 |

72 | ${outlet.description} 73 |

74 |

75 | ${outlet.categories.map((category) => category.name).join(' | ')} 76 |

77 |

Star Icon ${ 78 | outlet.rating 79 | }

80 |
81 |
82 |
83 | `; 84 | }; 85 | 86 | const foodMenu = (menu) => { 87 | return ` 88 | 101 | `; 102 | }; 103 | 104 | const drinkMenu = (menu) => { 105 | return ` 106 | 119 | `; 120 | }; 121 | 122 | const reviewOutlet = (review) => { 123 | return ` 124 |
125 |

${review.review}

126 |
127 | Profile Photo 128 |
129 |

${review.name}

130 |

${review.date}

131 |
132 |
133 |
134 | `; 135 | }; 136 | 137 | const createLikeOuletTemplate = () => ` 138 | 141 | `; 142 | 143 | const createUnlikeOuletTemplate = () => ` 144 | 147 | `; 148 | 149 | export { 150 | listOutlet, 151 | detailOutlet, 152 | foodMenu, 153 | drinkMenu, 154 | reviewOutlet, 155 | createLikeOuletTemplate, 156 | createUnlikeOuletTemplate, 157 | createSkeletonUi, 158 | }; 159 | -------------------------------------------------------------------------------- /src/scripts/views/templates/local-template.js: -------------------------------------------------------------------------------- 1 | const allFoodData = (data) => { 2 | return ` 3 |
4 | ${data['name']} 5 |
6 |

${data['name']}

7 |

Location Logo${data['location']}

8 |

$${data['price']}

9 |
10 |
11 | `; 12 | }; 13 | 14 | const mostFoodData = (data) => { 15 | return ` 16 |
17 | ${data['name']} 18 |
19 |

${data['name']}

20 |

Location Logo${data['location']}

21 |

$${data['price']}

22 |
23 | ${data['badge']} 24 |
25 | `; 26 | }; 27 | 28 | const skeletonFood = (count) => { 29 | let skeleton = ''; 30 | for (let i = 0; i < count; i += 1) { 31 | skeleton += ` 32 |
33 | skeleton image 34 |
35 |

36 |

37 |

38 |

39 |
40 |
41 | `; 42 | } 43 | return skeleton; 44 | }; 45 | 46 | const chooseBites = (data) => { 47 | return ` 48 |
49 | Choose Image 50 |
51 |
52 |

${data['title']}

53 |
54 | 55 | Arrow Logo 56 | ${data['reasonOne']} 57 | 58 | 59 | Arrow Logo 60 | ${data['reasonTwo']} 61 | 62 | 63 | Arrow Logo 64 | ${data['reasonThree']} 65 | 66 | 67 | Arrow Logo 68 | ${data['reasonFour']} 69 | 70 |
71 |
72 | `; 73 | }; 74 | 75 | export { mostFoodData, allFoodData, chooseBites, skeletonFood }; 76 | -------------------------------------------------------------------------------- /src/styles/detail.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #ff4f03; 3 | --dark-black: #1f1d36; 4 | --gold-color: #ffb700; 5 | --white-color: #ffffff; 6 | --gray-color: #737373; 7 | } 8 | 9 | li { 10 | list-style: none; 11 | } 12 | 13 | .wrapper { 14 | margin-top: 2.25rem; 15 | } 16 | 17 | .notfound-container { 18 | display: none; 19 | margin: 0 auto 9.375rem auto; 20 | text-align: center; 21 | } 22 | 23 | .notfound-container .notfound-image { 24 | width: 60%; 25 | } 26 | 27 | .notfound-container .notfound-title { 28 | font-size: 1.6em; 29 | } 30 | 31 | .notfound-container .notfound-description { 32 | font-size: 1em; 33 | color: var(--gray-color); 34 | } 35 | 36 | .btn { 37 | padding: 0.6rem 1.25rem; 38 | background-color: var(--primary-color); 39 | border: none; 40 | border-radius: 0.25rem; 41 | font-size: 1em; 42 | color: var(--white-color); 43 | cursor: pointer; 44 | transition: 300ms all; 45 | } 46 | 47 | .btn:hover { 48 | transform: translateY(-3px); 49 | -webkit-box-shadow: 2px 7px 20px -3px rgba(255, 204, 71, 0.8); 50 | -moz-box-shadow: 2px 7px 20px -3px rgba(255, 204, 71, 0.8); 51 | box-shadow: 2px 7px 20px -3px rgba(255, 204, 71, 0.8); 52 | } 53 | 54 | /* detail-outlet section */ 55 | 56 | .detail-outlet-section { 57 | display: flex; 58 | flex-direction: column; 59 | justify-content: center; 60 | align-items: center; 61 | } 62 | 63 | .detail-outlet-section .detail-outlet-name { 64 | font-size: 2.5em; 65 | color: var(--primary-color); 66 | letter-spacing: 0.056em; 67 | text-align: center; 68 | } 69 | 70 | .detail-outlet-section .detail-outlet-location { 71 | margin-bottom: 5rem; 72 | color: var(--gray-color); 73 | font-size: 1em; 74 | font-weight: 300; 75 | text-align: center; 76 | } 77 | 78 | .detail-outlet-section .description-wrapper { 79 | display: flex; 80 | justify-content: center; 81 | align-items: flex-start; 82 | column-gap: 1.3rem; 83 | max-width: 100%; 84 | color: var(--primary-color); 85 | } 86 | 87 | .outlet-border { 88 | border: 1px solid var(--primary-color); 89 | border-radius: 0.75rem; 90 | min-width: 53%; 91 | height: 21.875rem; 92 | overflow: hidden; 93 | } 94 | 95 | .outlet-border .detail-outlet-image { 96 | display: block; 97 | width: 100%; 98 | height: 100%; 99 | border-radius: 0.75rem; 100 | object-fit: cover; 101 | object-position: center; 102 | clip-path: polygon( 103 | 50% 0%, 104 | 100% 0, 105 | 100% 43%, 106 | 100% 80%, 107 | 80% 100%, 108 | 32% 100%, 109 | 0 100%, 110 | 0 18%, 111 | 18% 0 112 | ); 113 | } 114 | 115 | .description-content .about-title { 116 | font-size: 1.6em; 117 | letter-spacing: 0.056em; 118 | font-weight: 600; 119 | margin-bottom: 0.25rem; 120 | } 121 | 122 | .description-wrapper .detail-outlet-description { 123 | display: block; 124 | margin-bottom: 2rem; 125 | text-overflow: ellipsis; 126 | word-wrap: break-word; 127 | overflow: hidden; 128 | max-height: 12em; 129 | font-size: 1em; 130 | font-weight: 400; 131 | line-height: 1.8em; 132 | } 133 | 134 | .description-wrapper .detail-outlet-category { 135 | display: inline-block; 136 | margin-bottom: 0.25rem; 137 | padding: 0.3rem 0.7rem; 138 | font-size: 1em; 139 | font-weight: 500; 140 | color: var(--white-color); 141 | border-radius: 0.25rem; 142 | background-color: var(--primary-color); 143 | } 144 | 145 | .detail-outlet-rating { 146 | padding: 0.5rem 0; 147 | font-size: 1.2em; 148 | font-weight: 500; 149 | border-radius: 3.125rem; 150 | color: var(--gold-color); 151 | } 152 | 153 | .like-btn { 154 | font-size: 1.125em; 155 | position: fixed; 156 | bottom: 3rem; 157 | right: 3rem; 158 | background-color: var(--primary-color); 159 | color: var(--white-color); 160 | border: 0; 161 | border-radius: 50%; 162 | width: 3.438rem; 163 | height: 3.438rem; 164 | cursor: pointer; 165 | display: flex; 166 | align-items: center; 167 | justify-content: center; 168 | transition: 300ms all ease-in-out; 169 | z-index: 1; 170 | } 171 | 172 | .like-btn img { 173 | max-width: 1.2rem; 174 | } 175 | 176 | .like-btn:hover { 177 | transform: translateY(-3px); 178 | -webkit-box-shadow: 2px 7px 20px -3px rgba(255, 204, 71, 0.8); 179 | -moz-box-shadow: 2px 7px 20px -3px rgba(255, 204, 71, 0.8); 180 | box-shadow: 2px 7px 20px -3px rgba(255, 204, 71, 0.8); 181 | } 182 | 183 | /* Food Section */ 184 | 185 | .menu { 186 | display: grid; 187 | grid-template-columns: repeat(3, minmax(1rem, 1fr)); 188 | column-gap: 2rem; 189 | row-gap: 1rem; 190 | margin: 8rem 0 1rem 0; 191 | max-width: 100%; 192 | } 193 | 194 | .menu-container { 195 | margin: 1rem auto 0 auto; 196 | } 197 | 198 | .list-menu-description { 199 | grid-column: span 1; 200 | } 201 | 202 | .list-menu-description .menu-title { 203 | font-size: 1.6em; 204 | font-weight: 600; 205 | color: var(--primary-color); 206 | } 207 | 208 | .list-menu-description .menu-description { 209 | margin-top: 0.5rem; 210 | font-size: 14px; 211 | } 212 | 213 | .list-menu-description .seeall-menu { 214 | display: inline-block; 215 | text-decoration: none; 216 | margin: 1rem 0; 217 | } 218 | 219 | .menu-wrapper { 220 | padding: 0.5rem; 221 | max-width: 100%; 222 | border-radius: 0.25rem; 223 | margin-bottom: 1rem; 224 | background-color: var(--white-color); 225 | -webkit-box-shadow: 2px 10px 48px -15px rgba(209, 209, 209, 1); 226 | -moz-box-shadow: 2px 10px 48px -15px rgba(209, 209, 209, 1); 227 | box-shadow: 2px 10px 48px -15px rgba(209, 209, 209, 1); 228 | } 229 | 230 | .menu-card { 231 | display: flex; 232 | align-items: center; 233 | gap: 0.8rem; 234 | padding: 0.188rem 0; 235 | } 236 | 237 | .menu-icon { 238 | display: inline-block; 239 | width: 3.3rem; 240 | border-radius: 50%; 241 | margin-bottom: 0.5rem; 242 | } 243 | 244 | .menu-list { 245 | font-weight: 400; 246 | width: 100%; 247 | } 248 | 249 | .menu-name { 250 | font-size: 1em; 251 | font-weight: 500; 252 | color: var(--dark-black); 253 | } 254 | 255 | .menu-location { 256 | display: flex; 257 | column-gap: 0.2rem; 258 | margin: 0.25rem 0; 259 | color: var(--gray-color); 260 | } 261 | 262 | .menu-buy { 263 | display: flex; 264 | justify-content: space-between; 265 | align-items: center; 266 | align-items: center; 267 | width: 100%; 268 | } 269 | 270 | .menu-buy .chart-wrapper { 271 | display: flex; 272 | align-items: center; 273 | justify-items: center; 274 | margin-left: auto; 275 | border-radius: 0.313rem; 276 | border: 0; 277 | background-color: var(--primary-color); 278 | } 279 | 280 | .chart-icon { 281 | max-width: 0.875rem; 282 | margin: 0.375rem; 283 | } 284 | 285 | .menu-price { 286 | font-size: 0.875em; 287 | font-weight: 400; 288 | color: var(--primary-color); 289 | } 290 | 291 | /* Customer Section */ 292 | 293 | .reviews { 294 | margin: 6.25rem 0; 295 | } 296 | 297 | .review-title { 298 | margin-bottom: 1.5rem; 299 | color: var(--primary-color); 300 | font-size: 2em; 301 | font-weight: 600; 302 | } 303 | 304 | .review-container { 305 | columns: 4; 306 | column-gap: 1.25rem; 307 | } 308 | 309 | .review-card { 310 | padding: 0.5rem; 311 | width: 100%; 312 | margin-bottom: auto; 313 | overflow: hidden; 314 | break-inside: avoid; 315 | } 316 | 317 | .review-card .desc { 318 | position: relative; 319 | background-color: var(--dark-black); 320 | border-radius: 0.25rem; 321 | word-wrap: break-word; 322 | } 323 | 324 | .desc .review-text { 325 | padding: 0.9rem; 326 | font-size: 0.875rem; 327 | color: var(--white-color); 328 | letter-spacing: 0.05em; 329 | font-weight: 300; 330 | line-height: 1.4rem; 331 | } 332 | 333 | .review-card .desc::after { 334 | content: ''; 335 | position: absolute; 336 | bottom: -1rem; 337 | left: 1.2rem; 338 | min-height: 2.2rem; 339 | min-width: 2rem; 340 | z-index: -1; 341 | background-color: var(--dark-black); 342 | clip-path: polygon(100% 0, 0 0, 51% 100%); 343 | } 344 | 345 | .review-card .profile { 346 | display: flex; 347 | align-items: center; 348 | column-gap: 0.8rem; 349 | margin-top: 1.6rem; 350 | margin-left: 0.438rem; 351 | padding: 0 0.3rem; 352 | } 353 | 354 | .profile .photo { 355 | max-width: 3.125rem; 356 | clip-path: circle(); 357 | } 358 | 359 | .profile .name { 360 | font-size: 1rem; 361 | font-weight: 500; 362 | letter-spacing: 1px; 363 | color: var(--dark-black); 364 | } 365 | 366 | .profile .date { 367 | font-size: 0.8rem; 368 | margin-top: 0.25rem; 369 | color: var(--gray-color); 370 | } 371 | 372 | /* Form */ 373 | .form-container { 374 | display: flex; 375 | flex-direction: column; 376 | justify-content: center; 377 | align-items: flex-start; 378 | margin-bottom: 6.25rem; 379 | max-width: 50%; 380 | max-height: 18.75rem; 381 | } 382 | 383 | .form-container .name-input { 384 | position: relative; 385 | width: 100%; 386 | margin-bottom: 1rem; 387 | } 388 | 389 | .form-container .detail-input { 390 | position: relative; 391 | width: 100%; 392 | } 393 | 394 | .form-container label { 395 | font-size: 1em; 396 | font-weight: 500; 397 | } 398 | 399 | .form-container input[type='text'], 400 | textarea { 401 | position: relative; 402 | margin-top: 0.5rem; 403 | display: block; 404 | padding: 0.75rem; 405 | border: 2px solid var(--primary-color); 406 | border-radius: 0.25rem; 407 | box-sizing: border-box; 408 | font-size: 1.125em; 409 | font-weight: 300; 410 | width: 100%; 411 | } 412 | 413 | .form-container input[type='text']:focus, 414 | textarea:focus { 415 | box-shadow: 0 0 8px 1px rgba(255, 204, 71, 0.8), 416 | 0 0 5px 2px rgba(255, 204, 71, 0.8); 417 | outline: none; 418 | } 419 | 420 | .count { 421 | visibility: hidden; 422 | } 423 | 424 | label.count { 425 | position: absolute; 426 | top: 54.7%; 427 | right: 0.8rem; 428 | } 429 | 430 | label.count, 431 | span { 432 | color: #aaa492; 433 | font-weight: 400; 434 | font-size: 0.875em; 435 | } 436 | 437 | .form-container textarea { 438 | resize: vertical; 439 | } 440 | 441 | .btn-form { 442 | margin: 0.9rem 0; 443 | } 444 | 445 | @media (max-width: 74.938rem) { 446 | .menu { 447 | grid-template-columns: repeat(2, minmax(1rem, 1fr)); 448 | } 449 | .outlet-border { 450 | border: 0; 451 | } 452 | .outlet-border .detail-outlet-image { 453 | clip-path: initial; 454 | } 455 | .list-menu-description { 456 | grid-column: span 2; 457 | width: 70%; 458 | } 459 | .list-menu-description .menu-title { 460 | font-size: 2rem; 461 | } 462 | .list-menu-description .menu-description { 463 | font-size: 1rem; 464 | } 465 | .review-container { 466 | columns: 3; 467 | } 468 | .outlet-border { 469 | min-width: 100%; 470 | } 471 | .form-container, 472 | .menu, 473 | .wrapper { 474 | margin-right: 2.25rem; 475 | margin-left: 2.25rem; 476 | } 477 | .detail-outlet-section .description-wrapper { 478 | flex-direction: column; 479 | } 480 | .description-content .about-title { 481 | margin-top: 1.875rem; 482 | } 483 | } 484 | 485 | @media (max-width: 65.625rem) { 486 | .notfound-container .notfound-image { 487 | width: 75%; 488 | } 489 | } 490 | 491 | @media (max-width: 50rem) { 492 | .list-menu-description .menu-title { 493 | font-size: 1.5em; 494 | } 495 | .list-menu-description { 496 | width: 100%; 497 | } 498 | .review-container { 499 | columns: 2; 500 | } 501 | .detail-outlet-section .detail-outlet-name { 502 | margin-top: 6.25rem; 503 | } 504 | } 505 | 506 | @media (max-width: 46.875rem) { 507 | .notfound-container .notfound-image { 508 | width: 100%; 509 | } 510 | } 511 | 512 | @media (max-width: 43.75rem) { 513 | .menu { 514 | grid-template-columns: repeat(1, minmax(1rem, 1fr)); 515 | } 516 | .list-menu-description { 517 | grid-column: inherit; 518 | } 519 | .form-container { 520 | max-width: 100%; 521 | } 522 | .menu { 523 | flex-direction: column; 524 | align-items: flex-start; 525 | } 526 | .menu-title { 527 | margin-left: inherit; 528 | margin-right: inherit; 529 | } 530 | .notfound-container { 531 | margin-bottom: 6.25rem; 532 | margin-left: 2.25rem; 533 | margin-right: 2.25rem; 534 | } 535 | .notfound-container .notfound-title { 536 | font-size: 1.8em; 537 | } 538 | .notfound-container .notfound-description { 539 | font-size: 1em; 540 | } 541 | } 542 | 543 | @media (max-width: 37rem) { 544 | .review-container { 545 | columns: 1; 546 | } 547 | .detail-outlet-section .detail-outlet-name { 548 | margin-top: 3rem; 549 | font-size: 2em; 550 | } 551 | .description-content .about-title { 552 | font-size: 1.3em; 553 | } 554 | } 555 | 556 | @media (max-width: 27.313rem) { 557 | .notfound-container .notfound-title { 558 | font-size: 1rem; 559 | } 560 | .notfound-container .notfound-description { 561 | font-size: 0.75rem; 562 | color: var(--gray-color); 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /src/styles/loading.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: none; 3 | } 4 | 5 | .lds-ellipsis { 6 | display: inline-block; 7 | position: relative; 8 | max-width: 5rem; 9 | max-height: 5rem; 10 | top: 40%; 11 | } 12 | 13 | .lds-ellipsis div { 14 | position: absolute; 15 | top: 2.063rem; 16 | width: 0.813rem; 17 | height: 0.813rem; 18 | border-radius: 50%; 19 | background: #ff8303; 20 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 21 | } 22 | 23 | .lds-ellipsis div:nth-child(1) { 24 | left: 0.5rem; 25 | animation: lds-ellipsis1 0.6s infinite; 26 | } 27 | 28 | .lds-ellipsis div:nth-child(2) { 29 | left: 0.5rem; 30 | animation: lds-ellipsis2 0.6s infinite; 31 | } 32 | 33 | .lds-ellipsis div:nth-child(3) { 34 | left: 2rem; 35 | animation: lds-ellipsis2 0.6s infinite; 36 | } 37 | 38 | .lds-ellipsis div:nth-child(4) { 39 | left: 3.5rem; 40 | animation: lds-ellipsis3 0.6s infinite; 41 | } 42 | 43 | .overlay { 44 | z-index: 99999; 45 | position: fixed; 46 | top: 0; 47 | bottom: 0; 48 | left: 0; 49 | right: 0; 50 | background-color: rgba(0, 0, 0, 0.7); 51 | text-align: center; 52 | } 53 | 54 | @keyframes lds-ellipsis1 { 55 | 0% { 56 | transform: scale(0); 57 | } 58 | 100% { 59 | transform: scale(1); 60 | } 61 | } 62 | 63 | @keyframes lds-ellipsis3 { 64 | 0% { 65 | transform: scale(1); 66 | } 67 | 100% { 68 | transform: scale(0); 69 | } 70 | } 71 | 72 | @keyframes lds-ellipsis2 { 73 | 0% { 74 | transform: translate(0, 0); 75 | } 76 | 100% { 77 | transform: translate(1.5rem, 0); 78 | } 79 | } -------------------------------------------------------------------------------- /src/styles/responsive.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --white-color: #ffffff; 3 | --primary-color: #ff4f03; 4 | --dark-black: #1b1a17; 5 | --gold-color: #ffcc47; 6 | } 7 | 8 | @media (max-width: 74.938rem) { 9 | .container { 10 | max-width: 100%; 11 | margin: 0; 12 | } 13 | .outlet-container .outlet-card:first-child, 14 | .outlet-container .outlet-card:nth-child(7), 15 | .outlet-container .outlet-card:nth-child(13) { 16 | grid-column: initial; 17 | } 18 | .outlet-container .outlet-card:nth-child(12), 19 | .outlet-container .outlet-card:nth-child(18), 20 | .outlet-container .outlet-card:nth-child(6) { 21 | grid-column: initial; 22 | } 23 | .navbar-wrapper { 24 | border: 0; 25 | } 26 | .logo { 27 | padding-left: 1.2rem; 28 | } 29 | .nav-list { 30 | padding-right: 1.2rem; 31 | } 32 | .hero { 33 | margin-top: 0; 34 | max-width: 100%; 35 | } 36 | .hero .hero-image { 37 | max-width: 100%; 38 | } 39 | .search-container .search { 40 | width: 40%; 41 | } 42 | .outlet { 43 | margin-right: 2.25rem; 44 | margin-left: 2.25rem; 45 | } 46 | .most-favorite { 47 | margin-right: 2.25rem; 48 | margin-left: 2.25rem; 49 | } 50 | .all-food { 51 | margin-right: 2.25rem; 52 | margin-left: 2.25rem; 53 | } 54 | .choose { 55 | margin-right: 2.25rem; 56 | margin-left: 2.25rem; 57 | } 58 | .footer-wrapper { 59 | margin: 0 2.25rem; 60 | } 61 | .pagenotfound-container .page-notfound { 62 | max-width: 80%; 63 | } 64 | } 65 | 66 | @media (max-width: 62.375rem) { 67 | /* .food-description { 68 | padding: 2rem 0.625rem; 69 | } */ 70 | .hero { 71 | border-radius: 0; 72 | } 73 | .hero .hero-image { 74 | border-radius: 0; 75 | } 76 | } 77 | 78 | @media (max-width: 62.063rem) { 79 | .search-container .search { 80 | width: 50%; 81 | } 82 | .social-items { 83 | padding: 0.375rem; 84 | font-size: 0.875rem; 85 | } 86 | .choose-container { 87 | flex-direction: column; 88 | } 89 | .choose-title { 90 | margin-top: 1.875rem; 91 | font-size: 1.3em; 92 | } 93 | .choose-description { 94 | max-width: 100%; 95 | } 96 | .list-choose-item { 97 | font-size: 1.188rem; 98 | } 99 | .footer-wrapper { 100 | flex-direction: column; 101 | } 102 | .bites-description { 103 | font-size: 1.3em; 104 | margin-bottom: 1.25rem; 105 | } 106 | .footer-content { 107 | margin-bottom: 1.875rem; 108 | } 109 | } 110 | 111 | @media (max-width: 55.125rem) { 112 | .hero .hero-title { 113 | font-size: 3.6rem; 114 | } 115 | } 116 | 117 | @media (max-width: 52.5rem) { 118 | .hero .hero-description { 119 | bottom: 10%; 120 | } 121 | .bites-excess { 122 | margin: 6.25rem auto; 123 | } 124 | .badge { 125 | font-size: 0.875rem; 126 | padding: 0.5rem 0.75rem; 127 | } 128 | .choose-title { 129 | font-size: 1.25em; 130 | } 131 | .list-choose-item { 132 | font-size: 1em; 133 | } 134 | .footer-title { 135 | font-size: 0.875em; 136 | } 137 | .footer-description { 138 | font-size: 0.5em; 139 | } 140 | } 141 | 142 | @media (max-width: 50rem) { 143 | .navbar { 144 | position: fixed; 145 | top: 0; 146 | width: 100%; 147 | z-index: 99; 148 | } 149 | .navbar.nav-colored { 150 | transition: all 0.5s; 151 | background-color: var(--white-color); 152 | box-shadow: 1px 19px 20px -15px rgba(176, 176, 176, 0.75); 153 | -webkit-box-shadow: 1px 19px 20px -15px rgba(176, 176, 176, 0.75); 154 | -moz-box-shadow: 1px 19px 20px -15px rgba(176, 176, 176, 0.75); 155 | } 156 | .navbar.nav-transparent { 157 | background-color: transparent; 158 | transition: all 0.5s; 159 | } 160 | .logo { 161 | position: fixed; 162 | left: 1.25rem; 163 | z-index: 1; 164 | } 165 | .hamburger-menu { 166 | position: fixed; 167 | right: 1.25rem; 168 | display: flex; 169 | flex-direction: column; 170 | justify-content: space-around; 171 | z-index: 2; 172 | } 173 | .navbar .nav-list { 174 | position: fixed; 175 | right: 0; 176 | left: 0; 177 | top: 0; 178 | max-width: 100%; 179 | min-height: 100%; 180 | align-items: center; 181 | justify-content: center; 182 | flex-direction: column; 183 | background-color: var(--dark-black); 184 | transform: translateY(-120%); 185 | transition: all 0.6s; 186 | opacity: 0; 187 | z-index: 1; 188 | } 189 | .hero { 190 | max-width: 100%; 191 | } 192 | .hero .hero-title { 193 | font-size: 3.2rem; 194 | top: 25%; 195 | } 196 | .account-wrapper { 197 | display: flex; 198 | z-index: 2; 199 | } 200 | .navbar .nav-list.slide { 201 | opacity: 1; 202 | transform: translateX(0); 203 | } 204 | .list-items { 205 | font-size: 1.3em; 206 | color: white; 207 | } 208 | .section-title { 209 | font-size: 1.6em; 210 | } 211 | .choose-title { 212 | margin-top: 1.875rem; 213 | font-size: 1em; 214 | } 215 | .link-container { 216 | margin-top: 2rem; 217 | } 218 | .outlet-image-wrapper { 219 | max-width: 100%; 220 | } 221 | .outlet-description { 222 | width: 100%; 223 | } 224 | } 225 | 226 | @media (max-width: 46.688rem) { 227 | .hero .hero-title { 228 | font-size: 2.5rem; 229 | } 230 | .hero .hero-description { 231 | font-size: 1rem; 232 | } 233 | } 234 | 235 | @media (max-width: 39.938rem) { 236 | .hero .hero-title { 237 | font-size: 2.1rem; 238 | } 239 | .hero .hero-description { 240 | font-size: 1rem; 241 | } 242 | } 243 | 244 | @media (max-width: 33.563rem) { 245 | .hero .hero-title { 246 | font-size: 1.8rem; 247 | } 248 | .hero .hero-description { 249 | font-size: 0.875rem; 250 | } 251 | } 252 | 253 | @media (max-width: 34.25rem) { 254 | .main-container { 255 | margin: 0; 256 | } 257 | .hero .hero-image { 258 | max-height: 37.5rem; 259 | } 260 | .hero .hero-title { 261 | top: 30%; 262 | } 263 | .hero .hero-description { 264 | bottom: 13%; 265 | } 266 | .hero .hero-image { 267 | max-height: 100%; 268 | } 269 | .search-container { 270 | margin-top: 1.875rem; 271 | } 272 | .search-container .search { 273 | width: 100%; 274 | } 275 | .outlet-container { 276 | width: 100%; 277 | } 278 | .outlet-image { 279 | height: 14.375rem; 280 | } 281 | .footer-container { 282 | margin: 0 1rem; 283 | } 284 | } 285 | 286 | @media (max-width: 27.375rem) { 287 | .hero .hero-title { 288 | font-size: 1.6rem; 289 | } 290 | .hero .hero-description { 291 | font-size: 0.75rem; 292 | } 293 | } 294 | 295 | @media (max-width: 25.313rem) { 296 | .hero .hero-title { 297 | font-size: 1.4rem; 298 | top: 30%; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/styles/skeleton.css: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | opacity: 0.7; 3 | animation: skeleton-loading 1s linear infinite alternate; 4 | } 5 | 6 | .skeleton-text { 7 | height: 0.5rem; 8 | margin-bottom: 0.25rem; 9 | border-radius: 0.2rem; 10 | } 11 | 12 | .skeleton-description { 13 | position: absolute; 14 | bottom: 0; 15 | left: 0; 16 | width: 100%; 17 | padding: 0.313rem 0.625rem; 18 | } 19 | 20 | .skeleton-text:first-child { 21 | margin-top: 0.25rem; 22 | } 23 | 24 | .skeleton-text:nth-child(1) { 25 | width: 20%; 26 | } 27 | 28 | .skeleton-text:nth-child(2) { 29 | width: 30%; 30 | } 31 | 32 | .skeleton-text:last-child { 33 | width: 10%; 34 | } 35 | 36 | @keyframes skeleton-loading { 37 | 0% { 38 | background-color: hsl(200, 20%, 70%); 39 | } 40 | 100% { 41 | background-color: hsl(200, 20%, 95%); 42 | } 43 | } -------------------------------------------------------------------------------- /src/styles/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Croissant+One&display=swap'); 3 | 4 | :root { 5 | --primary-color: #ff4f03; 6 | --dark-black: #1f1d36; 7 | --orange-soft: #ff8000; 8 | --white-color: #fff; 9 | --badge-color: #ff3d68; 10 | --gold-color: #ffb700; 11 | --blue-gray: #616161; 12 | --border-accent: #ececec; 13 | } 14 | 15 | * { 16 | margin: 0; 17 | padding: 0; 18 | box-sizing: border-box; 19 | font-family: 'Poppins', sans-serif; 20 | color: var(--dark-black); 21 | } 22 | 23 | html { 24 | scroll-behavior: smooth; 25 | } 26 | 27 | body { 28 | min-height: 100vh; 29 | background-color: var(--white-color); 30 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity='0.1'%3E%3Cpath d='M24.37 16c.2.65.39 1.32.54 2H21.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06A5 5 0 0 1-17.45 28v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H-20a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1L.9 19.22a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0L2.26 23h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM-13.82 27l16.37 4.91L18.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H-13.1z'/%3E%3Cpath id='path6_fill-copy' d='M284.37 16c.2.65.39 1.32.54 2H281.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06a5 5 0 0 1-2.24-8.94v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H240a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM246.18 27l16.37 4.91L278.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H246.9z'/%3E%3Cpath d='M159.5 21.02A9 9 0 0 0 151 15h-42a9 9 0 0 0-8.5 6.02 6 6 0 0 0 .02 11.96A8.99 8.99 0 0 0 109 45h42a9 9 0 0 0 8.48-12.02 6 6 0 0 0 .02-11.96zM151 17h-42a7 7 0 0 0-6.33 4h54.66a7 7 0 0 0-6.33-4zm-9.34 26a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-7a7 7 0 1 1 0-14h42a7 7 0 1 1 0 14h-9.34zM109 27a9 9 0 0 0-7.48 4H101a4 4 0 1 1 0-8h58a4 4 0 0 1 0 8h-.52a9 9 0 0 0-7.48-4h-42z'/%3E%3Cpath d='M39 115a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm6-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0zm-3-29v-2h8v-6H40a4 4 0 0 0-4 4v10H22l-1.33 4-.67 2h2.19L26 130h26l3.81-40H58l-.67-2L56 84H42v-6zm-4-4v10h2V74h8v-2h-8a2 2 0 0 0-2 2zm2 12h14.56l.67 2H22.77l.67-2H40zm13.8 4H24.2l3.62 38h22.36l3.62-38z'/%3E%3Cpath d='M129 92h-6v4h-6v4h-6v14h-3l.24 2 3.76 32h36l3.76-32 .24-2h-3v-14h-6v-4h-6v-4h-8zm18 22v-12h-4v4h3v8h1zm-3 0v-6h-4v6h4zm-6 6v-16h-4v19.17c1.6-.7 2.97-1.8 4-3.17zm-6 3.8V100h-4v23.8a10.04 10.04 0 0 0 4 0zm-6-.63V104h-4v16a10.04 10.04 0 0 0 4 3.17zm-6-9.17v-6h-4v6h4zm-6 0v-8h3v-4h-4v12h1zm27-12v-4h-4v4h3v4h1v-4zm-6 0v-8h-4v4h3v4h1zm-6-4v-4h-4v8h1v-4h3zm-6 4v-4h-4v8h1v-4h3zm7 24a12 12 0 0 0 11.83-10h7.92l-3.53 30h-32.44l-3.53-30h7.92A12 12 0 0 0 130 126z'/%3E%3Cpath d='M212 86v2h-4v-2h4zm4 0h-2v2h2v-2zm-20 0v.1a5 5 0 0 0-.56 9.65l.06.25 1.12 4.48a2 2 0 0 0 1.94 1.52h.01l7.02 24.55a2 2 0 0 0 1.92 1.45h4.98a2 2 0 0 0 1.92-1.45l7.02-24.55a2 2 0 0 0 1.95-1.52L224.5 96l.06-.25a5 5 0 0 0-.56-9.65V86a14 14 0 0 0-28 0zm4 0h6v2h-9a3 3 0 1 0 0 6H223a3 3 0 1 0 0-6H220v-2h2a12 12 0 1 0-24 0h2zm-1.44 14l-1-4h24.88l-1 4h-22.88zm8.95 26l-6.86-24h18.7l-6.86 24h-4.98zM150 242a22 22 0 1 0 0-44 22 22 0 0 0 0 44zm24-22a24 24 0 1 1-48 0 24 24 0 0 1 48 0zm-28.38 17.73l2.04-.87a6 6 0 0 1 4.68 0l2.04.87a2 2 0 0 0 2.5-.82l1.14-1.9a6 6 0 0 1 3.79-2.75l2.15-.5a2 2 0 0 0 1.54-2.12l-.19-2.2a6 6 0 0 1 1.45-4.46l1.45-1.67a2 2 0 0 0 0-2.62l-1.45-1.67a6 6 0 0 1-1.45-4.46l.2-2.2a2 2 0 0 0-1.55-2.13l-2.15-.5a6 6 0 0 1-3.8-2.75l-1.13-1.9a2 2 0 0 0-2.5-.8l-2.04.86a6 6 0 0 1-4.68 0l-2.04-.87a2 2 0 0 0-2.5.82l-1.14 1.9a6 6 0 0 1-3.79 2.75l-2.15.5a2 2 0 0 0-1.54 2.12l.19 2.2a6 6 0 0 1-1.45 4.46l-1.45 1.67a2 2 0 0 0 0 2.62l1.45 1.67a6 6 0 0 1 1.45 4.46l-.2 2.2a2 2 0 0 0 1.55 2.13l2.15.5a6 6 0 0 1 3.8 2.75l1.13 1.9a2 2 0 0 0 2.5.8zm2.82.97a4 4 0 0 1 3.12 0l2.04.87a4 4 0 0 0 4.99-1.62l1.14-1.9a4 4 0 0 1 2.53-1.84l2.15-.5a4 4 0 0 0 3.09-4.24l-.2-2.2a4 4 0 0 1 .97-2.98l1.45-1.67a4 4 0 0 0 0-5.24l-1.45-1.67a4 4 0 0 1-.97-2.97l.2-2.2a4 4 0 0 0-3.09-4.25l-2.15-.5a4 4 0 0 1-2.53-1.84l-1.14-1.9a4 4 0 0 0-5-1.62l-2.03.87a4 4 0 0 1-3.12 0l-2.04-.87a4 4 0 0 0-4.99 1.62l-1.14 1.9a4 4 0 0 1-2.53 1.84l-2.15.5a4 4 0 0 0-3.09 4.24l.2 2.2a4 4 0 0 1-.97 2.98l-1.45 1.67a4 4 0 0 0 0 5.24l1.45 1.67a4 4 0 0 1 .97 2.97l-.2 2.2a4 4 0 0 0 3.09 4.25l2.15.5a4 4 0 0 1 2.53 1.84l1.14 1.9a4 4 0 0 0 5 1.62l2.03-.87zM152 207a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6 2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11 1a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-6 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-8 8a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4 7a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5-2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5 4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-5-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-24 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm16 5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm7-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0zm86-29a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1 246 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM275 214a29 29 0 0 0-57.97 0h57.96zM72.33 198.12c-.21-.32-.34-.7-.34-1.12v-12h-2v12a4.01 4.01 0 0 0 7.09 2.54c.57-.69.91-1.57.91-2.54v-12h-2v12a1.99 1.99 0 0 1-2 2 2 2 0 0 1-1.66-.88zM75 176c.38 0 .74-.04 1.1-.12a4 4 0 0 0 6.19 2.4A13.94 13.94 0 0 1 84 185v24a6 6 0 0 1-6 6h-3v9a5 5 0 1 1-10 0v-9h-3a6 6 0 0 1-6-6v-24a14 14 0 0 1 14-14 5 5 0 0 0 5 5zm-17 15v12a1.99 1.99 0 0 0 1.22 1.84 2 2 0 0 0 2.44-.72c.21-.32.34-.7.34-1.12v-12h2v12a3.98 3.98 0 0 1-5.35 3.77 3.98 3.98 0 0 1-.65-.3V209a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-24c.01-1.53-.23-2.88-.72-4.17-.43.1-.87.16-1.28.17a6 6 0 0 1-5.2-3 7 7 0 0 1-6.47-4.88A12 12 0 0 0 58 185v6zm9 24v9a3 3 0 1 0 6 0v-9h-6z'/%3E%3Cpath d='M-17 191a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2H4zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1-14 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM15 214a29 29 0 0 0-57.97 0h57.96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); 31 | background-repeat: repeat; 32 | background-position: center; 33 | background-attachment: fixed; 34 | } 35 | 36 | .pagenotfound-container { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | margin: 6.25rem 1rem; 42 | } 43 | 44 | .pagenotfound-container .page-notfound { 45 | max-width: 35%; 46 | } 47 | 48 | .pagenotfound-container .home-link { 49 | font-size: 1.6em; 50 | font-weight: 500; 51 | } 52 | 53 | .pagenotfound-container .home-link:hover { 54 | color: #ffc801; 55 | } 56 | 57 | .pagenotfound-container p { 58 | font-size: 1em; 59 | color: var(--blue-gray); 60 | text-align: center; 61 | } 62 | 63 | /* Skip to content */ 64 | 65 | .skip-link { 66 | position: absolute; 67 | top: -3rem; 68 | left: 0; 69 | background-color: var(--primary-color); 70 | color: var(--white-color); 71 | padding: 0.5rem; 72 | z-index: 99; 73 | } 74 | 75 | .skip-link:focus { 76 | top: 0.125rem; 77 | left: 0.125rem; 78 | border: 2px solid var(--dark-black); 79 | } 80 | 81 | .container { 82 | max-width: 62.5rem; 83 | margin: 0 auto; 84 | } 85 | 86 | /* Navigation Drawer */ 87 | 88 | header { 89 | margin-bottom: 6rem; 90 | } 91 | 92 | .navbar-wrapper { 93 | border-bottom: 1px solid var(--border-accent); 94 | } 95 | 96 | .navbar { 97 | display: flex; 98 | justify-content: space-between; 99 | align-items: center; 100 | padding: 0.625rem 0; 101 | max-width: 62.5rem; 102 | height: 3.75rem; 103 | margin: 0 auto; 104 | } 105 | 106 | .navbar .logo { 107 | min-width: 2.75rem; 108 | } 109 | 110 | .hamburger-menu { 111 | position: relative; 112 | align-self: center; 113 | margin-right: 1.25rem; 114 | display: none; 115 | min-width: 2.75rem; 116 | min-height: 2.75rem; 117 | } 118 | 119 | .hamburger-menu:focus { 120 | border: 1px solid var(--primary-color); 121 | } 122 | 123 | .check { 124 | position: absolute; 125 | min-width: 2.75rem; 126 | min-height: 2.75rem; 127 | top: 50%; 128 | left: 50%; 129 | transform: translate(-50%, -50%); 130 | opacity: 0; 131 | cursor: pointer; 132 | z-index: 3; 133 | } 134 | 135 | .hamburger-menu span { 136 | display: block; 137 | min-width: 1.75rem; 138 | min-height: 0.375rem; 139 | background-color: var(--primary-color); 140 | border-radius: 3.125rem; 141 | transition: all 0.3s; 142 | } 143 | 144 | /* Drawer Menu Animation */ 145 | 146 | .hamburger-menu span:nth-child(2) { 147 | transform-origin: 0 0; 148 | } 149 | 150 | .hamburger-menu span:nth-child(4) { 151 | transform-origin: 0 100%; 152 | } 153 | 154 | .hamburger-menu input:checked ~ span:nth-child(2) { 155 | transform: rotate(45deg); 156 | } 157 | 158 | .hamburger-menu input:checked ~ span:nth-child(4) { 159 | transform: rotate(-45deg); 160 | } 161 | 162 | .hamburger-menu input:checked ~ span:nth-child(3) { 163 | opacity: 0; 164 | transform: translateX(-3.125rem); 165 | } 166 | 167 | /* Nav Item */ 168 | 169 | .nav-list { 170 | display: flex; 171 | justify-content: center; 172 | list-style: none; 173 | gap: 1.875rem; 174 | } 175 | 176 | .list-items { 177 | position: relative; 178 | min-height: 2.75rem; 179 | min-width: 2.75rem; 180 | text-decoration: none; 181 | font-size: 1.3em; 182 | color: var(--dark-black); 183 | padding: 1rem 0; 184 | transition: all 0.28s; 185 | } 186 | 187 | .list-items.active { 188 | color: var(--primary-color); 189 | } 190 | 191 | .list-items:hover { 192 | color: var(--primary-color); 193 | } 194 | 195 | .list-items::after { 196 | content: ''; 197 | position: absolute; 198 | bottom: 0.6rem; 199 | left: 50%; 200 | transform: translateX(-50%); 201 | min-height: 0.188rem; 202 | min-width: 0; 203 | border-radius: 0.625rem; 204 | background-color: var(--primary-color); 205 | transition: all 0.28s; 206 | } 207 | 208 | .list-items:hover::after { 209 | min-width: 100%; 210 | } 211 | 212 | .account-wrapper { 213 | display: none; 214 | gap: 2rem; 215 | margin-top: 2rem; 216 | } 217 | 218 | .social-media { 219 | padding: 0.8rem; 220 | color: var(--white-color); 221 | } 222 | 223 | .social-media .fab { 224 | max-width: 1.6rem; 225 | color: var(--white-color); 226 | transition: 0.4s; 227 | } 228 | 229 | .social-media .fab:hover { 230 | transform: translateY(-5px); 231 | } 232 | 233 | .social-media .fa-instagram:hover { 234 | color: #833ab4; 235 | } 236 | 237 | .social-media .fa-twitter:hover { 238 | color: #1da1f2; 239 | } 240 | 241 | .social-media .fa-github:hover { 242 | color: #ff501b; 243 | } 244 | 245 | /* Hero */ 246 | 247 | .hero { 248 | display: none; 249 | position: relative; 250 | margin: 1.875rem auto 0 auto; 251 | width: 62.5rem; 252 | max-height: 100%; 253 | background-image: linear-gradient( 254 | to right top, 255 | #636363, 256 | #565656, 257 | #494949, 258 | #3c3c3c, 259 | #303030 260 | ); 261 | background-attachment: fixed; 262 | border-radius: 0.5rem; 263 | } 264 | 265 | .hero .hero-image { 266 | display: block; 267 | height: 30rem; 268 | width: 100%; 269 | object-fit: cover; 270 | object-position: center; 271 | border-radius: 0.5rem; 272 | mix-blend-mode: overlay; 273 | } 274 | 275 | .hero .hero-title { 276 | position: absolute; 277 | top: 14%; 278 | left: 0; 279 | right: 0; 280 | width: 100%; 281 | text-align: center; 282 | font-family: 'Croissant One', cursive; 283 | font-weight: 600; 284 | font-size: 4rem; 285 | letter-spacing: 0.125em; 286 | color: var(--primary-color); 287 | text-shadow: 0 4px 4px rgba(0, 0, 0, 0.7); 288 | } 289 | 290 | .hero .hero-description { 291 | position: absolute; 292 | width: 100%; 293 | margin: 0 auto; 294 | left: 0; 295 | right: 0; 296 | bottom: 8%; 297 | max-width: 80%; 298 | font-size: 1.2rem; 299 | font-weight: 300; 300 | line-height: 2em; 301 | color: var(--white-color); 302 | text-shadow: 0 4px 4px rgba(0, 0, 0, 0.7); 303 | } 304 | 305 | /* Bites Excess */ 306 | 307 | .bites-excess { 308 | display: grid; 309 | grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); 310 | gap: 1rem; 311 | align-items: center; 312 | justify-content: center; 313 | margin: 9.375rem auto; 314 | min-width: 100%; 315 | } 316 | 317 | .bites-excess .excess { 318 | position: relative; 319 | margin: 0 auto; 320 | padding-left: 3rem; 321 | width: 100%; 322 | } 323 | 324 | .bites-excess .excess:first-child::after { 325 | content: ''; 326 | position: absolute; 327 | top: 50%; 328 | right: 0; 329 | transform: translateY(-50%); 330 | height: 60%; 331 | width: 1px; 332 | background-color: var(--primary-color); 333 | } 334 | 335 | .bites-excess .excess:last-child::before { 336 | content: ''; 337 | position: absolute; 338 | top: 50%; 339 | left: 0; 340 | transform: translateY(-50%); 341 | height: 60%; 342 | width: 1px; 343 | background-color: var(--primary-color); 344 | } 345 | 346 | .excess .excesss-number { 347 | font-size: 4em; 348 | color: var(--primary-color); 349 | } 350 | 351 | .excess .excess-title { 352 | margin: 0.25rem 0 0.5rem 0; 353 | font-weight: 600; 354 | color: var(--dark-black); 355 | } 356 | 357 | .excess .excess-description { 358 | font-size: 1em; 359 | color: var(--blue-gray); 360 | } 361 | 362 | /* Main Content */ 363 | 364 | .section-title { 365 | font-family: 'Croissant One', cursive; 366 | font-size: 2em; 367 | text-align: center; 368 | font-weight: 600; 369 | color: var(--primary-color); 370 | } 371 | 372 | .line-style { 373 | font-family: 'Croissant One', cursive; 374 | background-color: var(--primary-color); 375 | color: var(--white-color); 376 | border-top-right-radius: 1rem; 377 | border-bottom-left-radius: 1rem; 378 | border-top-left-radius: 0.25rem; 379 | border-bottom-right-radius: 0.25rem; 380 | padding: 0 0.5rem; 381 | letter-spacing: 1px; 382 | font-size: 1em; 383 | font-weight: 600; 384 | } 385 | 386 | /* Most Favorite Section */ 387 | 388 | .most-favorite { 389 | margin-top: 9.375rem; 390 | text-align: right; 391 | } 392 | 393 | .link-container { 394 | display: inline-block; 395 | } 396 | 397 | .link-container .fas { 398 | color: var(--primary-color); 399 | transition: 0.5s; 400 | margin-left: 0.4rem; 401 | } 402 | 403 | .link-container a.food-link { 404 | display: inline-block; 405 | padding: 0.563rem 0; 406 | text-decoration: none; 407 | font-size: 1.125em; 408 | font-weight: 600; 409 | color: var(--primary-color); 410 | } 411 | 412 | .food { 413 | display: grid; 414 | grid-template-columns: repeat(auto-fit, minmax(15.625rem, 1fr)); 415 | gap: 0.75rem; 416 | margin-top: 1rem; 417 | text-align: left; 418 | } 419 | 420 | .food .card { 421 | position: relative; 422 | max-width: 100%; 423 | max-height: 100%; 424 | background-color: var(--white-color); 425 | background-color: var(--white-color); 426 | -webkit-box-shadow: 2px 10px 48px -15px rgba(209, 209, 209, 1); 427 | -moz-box-shadow: 2px 10px 48px -15px rgba(209, 209, 209, 1); 428 | box-shadow: 2px 10px 48px -15px rgba(209, 209, 209, 1); 429 | border-radius: 0.5rem; 430 | overflow: hidden; 431 | } 432 | 433 | .food-image { 434 | position: relative; 435 | display: block; 436 | width: 100%; 437 | height: 100%; 438 | object-fit: cover; 439 | object-position: center; 440 | border-radius: 0.5rem; 441 | transition: 0.4s; 442 | transform: scale(1.1); 443 | } 444 | 445 | .card:hover .food-image { 446 | transform: scale(1); 447 | } 448 | 449 | .food-description { 450 | position: absolute; 451 | bottom: 0; 452 | left: 0; 453 | display: flex; 454 | flex-direction: column; 455 | justify-content: flex-end; 456 | height: 60%; 457 | width: 100%; 458 | padding: 0 0.625rem; 459 | background-image: linear-gradient(to top, #202020, transparent); 460 | } 461 | 462 | .food-title { 463 | font-size: 1.5em; 464 | font-weight: 600; 465 | color: var(--white-color); 466 | text-shadow: 0 4px 4px rgba(0, 0, 0, 0.5); 467 | } 468 | 469 | .location { 470 | display: flex; 471 | align-items: center; 472 | font-weight: 400; 473 | color: var(--white-color); 474 | margin-bottom: 0.25rem; 475 | } 476 | 477 | .location-icon { 478 | margin-right: 0.313rem; 479 | max-width: 0.7rem; 480 | color: var(--white-color); 481 | } 482 | 483 | .price { 484 | font-weight: 500; 485 | color: var(--primary-color); 486 | margin-bottom: 0.313rem; 487 | } 488 | 489 | .badge { 490 | position: absolute; 491 | top: 0; 492 | right: 0; 493 | background: var(--badge-color); 494 | max-width: max-content; 495 | padding: 0.5rem 0.75rem; 496 | font-weight: 500; 497 | font-size: 0.875rem; 498 | color: var(--white-color); 499 | border-top-right-radius: 0.5rem; 500 | border-bottom-left-radius: 0.5rem; 501 | box-shadow: 0 43px 181px rgba(27, 26, 23, 0.07), 502 | 0 19.1268px 79.4118px rgba(27, 26, 23, 0.0456112), 503 | 0 13.3398px 53.9366px rgba(27, 26, 23, 0.035), 504 | 0 7.20765px 29.2138px rgba(27, 26, 23, 0.0243888); 505 | opacity: 0.9; 506 | } 507 | 508 | /* All Food Section */ 509 | 510 | .all-food { 511 | margin-top: 5.125rem; 512 | } 513 | 514 | .another-food { 515 | margin-bottom: 2.563rem; 516 | } 517 | 518 | /* Outlest Section */ 519 | 520 | .outlet { 521 | margin-top: 5.125rem; 522 | text-align: right; 523 | } 524 | 525 | .search-container { 526 | display: flex; 527 | flex-direction: column; 528 | align-items: flex-start; 529 | max-width: 100%; 530 | } 531 | 532 | .search-container label { 533 | margin-bottom: 0.3rem; 534 | font-weight: 500; 535 | font-size: 1em; 536 | color: var(--primary-color); 537 | } 538 | 539 | .search-container .search { 540 | display: inline-block; 541 | padding: 0.5rem 0.75rem; 542 | width: 28%; 543 | font-size: 1em; 544 | font-weight: 400; 545 | color: var(--dark-black); 546 | border-radius: 0.25rem; 547 | border: 2px solid var(--primary-color); 548 | } 549 | 550 | .search-container .search:focus { 551 | box-shadow: 0 0 8px 1px rgba(255, 204, 71, 0.8), 552 | 0 0 5px 2px rgba(255, 204, 71, 0.8); 553 | outline: none; 554 | } 555 | 556 | .link-container .outlet-link { 557 | display: none; 558 | text-decoration: none; 559 | padding: 0.563rem 0; 560 | text-align: right; 561 | font-size: 1.125em; 562 | font-weight: 500; 563 | color: var(--primary-color); 564 | } 565 | 566 | .outlet-container { 567 | display: grid; 568 | grid-template-columns: repeat(auto-fit, minmax(14.375rem, 1fr)); 569 | column-gap: 1.25rem; 570 | margin-top: 1rem; 571 | row-gap: 0.8rem; 572 | text-align: left; 573 | } 574 | 575 | .outlet-container .outlet-card { 576 | position: relative; 577 | border-radius: 0.75rem; 578 | max-width: 33.688rem; 579 | } 580 | 581 | .outlet-container .outlet-card:first-child, 582 | .outlet-container .outlet-card:nth-child(7), 583 | .outlet-container .outlet-card:nth-child(13) { 584 | grid-column: 1 / span 2; 585 | } 586 | 587 | .outlet-container .outlet-card:nth-child(12), 588 | .outlet-container .outlet-card:nth-child(18), 589 | .outlet-container .outlet-card:nth-child(6) { 590 | grid-column: 3 / span 2; 591 | } 592 | 593 | .outlet-image-wrapper { 594 | position: relative; 595 | max-width: 100%; 596 | overflow: hidden; 597 | border-radius: 0.5rem; 598 | } 599 | 600 | .outlet-image-wrapper .outlet-image { 601 | position: relative; 602 | display: block; 603 | width: 100%; 604 | height: 12.5rem; 605 | object-fit: cover; 606 | object-position: center; 607 | border-radius: 0.5rem; 608 | transform: scale(1.1); 609 | transition: 0.4s; 610 | } 611 | 612 | .outlet-image-wrapper .outlet-image:hover { 613 | transform: scale(1); 614 | } 615 | 616 | .outlet-wrapper { 617 | padding: 0.188rem; 618 | } 619 | 620 | .outlet-name { 621 | display: inline-block; 622 | font-size: 1.1em; 623 | font-weight: 500; 624 | margin-bottom: 0.188rem; 625 | } 626 | 627 | .outlet-name a { 628 | padding: 0.625rem 0; 629 | text-decoration: none; 630 | } 631 | 632 | .outlet-name:hover { 633 | text-decoration: underline; 634 | } 635 | 636 | .outlet-location { 637 | display: flex; 638 | align-items: center; 639 | margin-bottom: 0.188rem; 640 | font-weight: 400; 641 | color: var(--blue-gray); 642 | } 643 | 644 | .outlet-description { 645 | display: block; 646 | white-space: nowrap; 647 | overflow: hidden; 648 | text-overflow: ellipsis; 649 | max-width: max-content; 650 | } 651 | 652 | .outlet-rating { 653 | display: flex; 654 | align-items: center; 655 | margin-top: 0.313rem; 656 | font-weight: 500; 657 | color: var(--gold-color); 658 | } 659 | 660 | .star { 661 | margin-right: 0.313rem; 662 | align-self: flex-start; 663 | margin-top: 0.063rem; 664 | } 665 | 666 | /* Choose Section */ 667 | 668 | .choose { 669 | margin: 9.375rem auto; 670 | } 671 | 672 | .choose-container { 673 | display: flex; 674 | justify-content: flex-start; 675 | align-items: flex-start; 676 | gap: 1.875rem; 677 | margin-top: 3.125rem; 678 | } 679 | 680 | .choose-outline { 681 | max-width: 100%; 682 | border: 2px solid var(--primary-color); 683 | border-radius: 0.75rem; 684 | } 685 | 686 | .choose-image { 687 | display: block; 688 | width: 100%; 689 | height: 100%; 690 | clip-path: polygon( 691 | 50% 0%, 692 | 100% 0, 693 | 100% 43%, 694 | 100% 80%, 695 | 80% 100%, 696 | 32% 100%, 697 | 0 100%, 698 | 0 18%, 699 | 18% 0 700 | ); 701 | border-radius: 0.5rem; 702 | } 703 | 704 | .choose-title { 705 | font-size: 1.875em; 706 | } 707 | 708 | .list-choose { 709 | margin-top: 1.25rem; 710 | } 711 | 712 | .list-choose-item { 713 | display: flex; 714 | align-items: center; 715 | column-gap: 1.25rem; 716 | margin-bottom: 1rem; 717 | font-size: 1.125em; 718 | color: var(--dark-black); 719 | } 720 | 721 | .arrow-icon { 722 | max-width: 1.625rem; 723 | } 724 | 725 | /* Footer Section */ 726 | 727 | .footer-container { 728 | border-top: 1px solid var(--border-accent); 729 | } 730 | 731 | .footer-wrapper { 732 | display: flex; 733 | justify-content: space-between; 734 | align-items: flex-start; 735 | padding: 3.125rem 0; 736 | max-width: 62.5rem; 737 | margin: 0 auto; 738 | } 739 | 740 | .footer-main { 741 | text-align: start; 742 | } 743 | 744 | .footer-title { 745 | font-size: 1.1em; 746 | margin-bottom: 0.5rem; 747 | } 748 | 749 | .bites-description { 750 | font-size: 1.1em; 751 | font-weight: 500; 752 | } 753 | 754 | .list-footer { 755 | list-style: none; 756 | } 757 | 758 | .footer-item { 759 | margin-bottom: 0.313rem; 760 | } 761 | 762 | .copyright { 763 | text-align: center; 764 | padding-bottom: 3.125rem; 765 | font-size: 1.125rem; 766 | font-weight: 500; 767 | } 768 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Bites 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /steps.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | type steps_file = typeof import('./steps_file.js'); 3 | 4 | declare namespace CodeceptJS { 5 | interface SupportObject { I: I, current: any } 6 | interface Methods extends Puppeteer {} 7 | interface I extends ReturnType {} 8 | namespace Translation { 9 | interface Actions {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /steps_file.js: -------------------------------------------------------------------------------- 1 | // in this file you can append custom step methods to 'I' object 2 | 3 | module.exports = function() { 4 | return actor({ 5 | 6 | // Define custom steps here, use 'this' to access default methods of I. 7 | // It is recommended to place a general 'login' function here. 8 | 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const WebpackPwaManifest = require('webpack-pwa-manifest'); 5 | const ImageminPlugin = require('imagemin-webp-webpack-plugin'); 6 | const { InjectManifest } = require('workbox-webpack-plugin'); 7 | const BundleAnalyzerPlugin = 8 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 9 | 10 | module.exports = { 11 | entry: path.resolve(__dirname, 'src/scripts/index.js'), 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: [ 21 | { 22 | loader: 'style-loader', 23 | }, 24 | { 25 | loader: 'css-loader', 26 | }, 27 | ], 28 | }, 29 | { 30 | test: /\.(jpe?g|png|gif|woff|woff2|eot|ttf|svg)(\?[a-z0-9=.]+)?$/, 31 | loader: 'url-loader?limit=100000', 32 | }, 33 | ], 34 | }, 35 | optimization: { 36 | splitChunks: { 37 | chunks: 'all', 38 | minSize: 20000, 39 | maxSize: 70000, 40 | minChunks: 1, 41 | maxAsyncRequests: 30, 42 | maxInitialRequests: 30, 43 | automaticNameDelimiter: '~', 44 | enforceSizeThreshold: 50000, 45 | cacheGroups: { 46 | defaultVendors: { 47 | test: /[\\/]node_modules[\\/]/, 48 | priority: -10, 49 | }, 50 | default: { 51 | minChunks: 2, 52 | priority: -20, 53 | reuseExistingChunk: true, 54 | }, 55 | }, 56 | }, 57 | }, 58 | devServer: { 59 | disableHostCheck: true, 60 | port: 8080, 61 | }, 62 | plugins: [ 63 | new HtmlWebpackPlugin({ 64 | template: path.resolve(__dirname, 'src/templates/index.html'), 65 | filename: 'index.html', 66 | }), 67 | new CopyWebpackPlugin({ 68 | patterns: [ 69 | { 70 | from: path.resolve(__dirname, 'src/public/'), 71 | to: path.resolve(__dirname, 'dist/'), 72 | }, 73 | ], 74 | }), 75 | new InjectManifest({ 76 | swSrc: path.resolve(__dirname, 'src/scripts/sw.js'), 77 | }), 78 | new ImageminPlugin({ 79 | config: [ 80 | { 81 | test: /\.(jpe?g|png)(\?[a-z0-9=.]+)?$/, 82 | options: { 83 | quality: 50, 84 | }, 85 | }, 86 | ], 87 | overrideExtension: true, 88 | }), 89 | new WebpackPwaManifest({ 90 | name: 'Bites', 91 | short_name: 'Bites', 92 | description: 'Bites is a food delivery app', 93 | background_color: '#71DFE7', 94 | start_url: '/index.html', 95 | display: 'standalone', 96 | theme_color: '#ff8303', 97 | crossorigin: 'use-credentials', 98 | icons: [ 99 | { 100 | src: path.resolve('src/public/icons/icon-72x72.png'), 101 | size: '72x72', 102 | }, 103 | { 104 | src: path.resolve('src/public/icons/icon-96x96.png'), 105 | size: '96x96', 106 | }, 107 | { 108 | src: path.resolve('src/public/icons/icon-128x128.png'), 109 | size: '128x128', 110 | }, 111 | { 112 | src: path.resolve('src/public/icons/icon-144x144.png'), 113 | size: '144x144', 114 | }, 115 | { 116 | src: path.resolve('src/public/icons/icon-152x152.png'), 117 | size: '152x152', 118 | }, 119 | { 120 | src: path.resolve('src/public/icons/icon-192x192.png'), 121 | size: '192x192', 122 | }, 123 | { 124 | src: path.resolve('src/public/icons/icon-384x384.png'), 125 | size: '384x384', 126 | }, 127 | { 128 | src: path.resolve('src/public/icons/icon-512x512.png'), 129 | size: '512x512', 130 | }, 131 | ], 132 | }), 133 | new BundleAnalyzerPlugin({ 134 | analyzerMode: 'json', 135 | openAnalyzer: false, 136 | }), 137 | ], 138 | }; 139 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const path = require('path'); 3 | const common = require('./webpack.common'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devServer: { 8 | contentBase: path.resolve(__dirname, 'dist'), 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.js$/, 10 | exclude: '/node_modules/', 11 | use: [ 12 | { 13 | loader: 'babel-loader', 14 | options: { 15 | presets: ['@babel/preset-env'], 16 | }, 17 | }, 18 | ], 19 | }, 20 | ], 21 | }, 22 | }); 23 | --------------------------------------------------------------------------------