├── .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 |
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 |
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 | See all
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 |
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 | See all
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 |
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 |
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 |
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 | Search Outlet
10 |
11 |
12 | See all
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 |
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 |
14 |
15 |
16 |
17 |
18 |
21 | ${outlet.name || 'outlet not found'}
22 |
23 |
24 | ${
25 | outlet.city || 'outlet not found'
26 | }
27 |
28 | ${outlet.description}
29 |
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 |
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 |
68 |
69 |
70 | About ${outlet.name}
71 |
72 | ${outlet.description}
73 |
74 |
75 | ${outlet.categories.map((category) => category.name).join(' | ')}
76 |
77 | ${
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 |
126 |
127 |
128 |
129 |
${review.name}
130 |
${review.date}
131 |
132 |
133 |
134 | `;
135 | };
136 |
137 | const createLikeOuletTemplate = () => `
138 |
139 |
140 |
141 | `;
142 |
143 | const createUnlikeOuletTemplate = () => `
144 |
145 |
146 |
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 |
5 |
6 | ${data['name']}
7 | ${data['location']}
8 | $${data['price']}
9 |
10 |
11 | `;
12 | };
13 |
14 | const mostFoodData = (data) => {
15 | return `
16 |
17 |
18 |
19 | ${data['name']}
20 | ${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 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | `;
42 | }
43 | return skeleton;
44 | };
45 |
46 | const chooseBites = (data) => {
47 | return `
48 |
49 |
50 |
51 |
52 |
${data['title']}
53 |
54 |
55 |
56 | ${data['reasonOne']}
57 |
58 |
59 |
60 | ${data['reasonTwo']}
61 |
62 |
63 |
64 | ${data['reasonThree']}
65 |
66 |
67 |
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 |
--------------------------------------------------------------------------------