├── chapter11-tools
├── npm
│ ├── myScript.js
│ ├── src
│ │ ├── image
│ │ │ └── spinner.gif
│ │ ├── js
│ │ │ ├── spinner.js
│ │ │ └── main.js
│ │ └── css
│ │ │ └── main.css
│ ├── package.json
│ ├── README.md
│ ├── data
│ │ ├── latest.json
│ │ └── top.json
│ └── index.html
├── babel
│ ├── myScript.js
│ ├── src
│ │ ├── image
│ │ │ └── spinner.gif
│ │ ├── js
│ │ │ ├── spinner.js
│ │ │ ├── polyfill
│ │ │ │ └── append.js
│ │ │ └── main.js
│ │ └── css
│ │ │ └── main.css
│ ├── babel.config.json
│ ├── webpack.config.js
│ ├── README.md
│ ├── webpack.dev.config.js
│ ├── data
│ │ ├── latest.json
│ │ └── top.json
│ ├── package.json
│ ├── webpack.prod.config.js
│ └── index.html
├── bundler
│ ├── myScript.js
│ ├── src
│ │ ├── image
│ │ │ └── spinner.gif
│ │ ├── js
│ │ │ ├── spinner.js
│ │ │ └── main.js
│ │ └── css
│ │ │ └── main.css
│ ├── webpack.config.js
│ ├── README.md
│ ├── webpack.dev.config.js
│ ├── package.json
│ ├── data
│ │ ├── latest.json
│ │ └── top.json
│ ├── webpack.prod.config.js
│ └── index.html
├── lint
│ ├── myScript.js
│ ├── src
│ │ ├── scss
│ │ │ ├── index.scss
│ │ │ ├── _vars.scss
│ │ │ ├── _spinner.scss
│ │ │ ├── _base.scss
│ │ │ ├── _flex.scss
│ │ │ └── _main.scss
│ │ ├── image
│ │ │ └── spinner.gif
│ │ ├── js
│ │ │ ├── spinner.js
│ │ │ ├── polyfill
│ │ │ │ └── append.js
│ │ │ └── main.js
│ │ └── css
│ │ │ └── main.css
│ ├── .prettierrc
│ ├── babel.config.json
│ ├── webpack.config.js
│ ├── .eslintrc.js
│ ├── .stylelintrc.js
│ ├── README.md
│ ├── webpack.dev.config.js
│ ├── data
│ │ ├── latest.json
│ │ └── top.json
│ ├── webpack.prod.config.js
│ ├── package.json
│ └── index.html
└── sass
│ ├── myScript.js
│ ├── src
│ ├── scss
│ │ ├── index.scss
│ │ ├── _vars.scss
│ │ ├── _spinner.scss
│ │ ├── _base.scss
│ │ ├── _flex.scss
│ │ └── _main.scss
│ ├── image
│ │ └── spinner.gif
│ ├── js
│ │ ├── spinner.js
│ │ ├── polyfill
│ │ │ └── append.js
│ │ └── main.js
│ └── css
│ │ └── main.css
│ ├── babel.config.json
│ ├── webpack.config.js
│ ├── README.md
│ ├── webpack.dev.config.js
│ ├── data
│ ├── latest.json
│ └── top.json
│ ├── package.json
│ ├── webpack.prod.config.js
│ └── index.html
├── chapter13-test
├── .storybook
│ ├── preview.js
│ └── main.js
├── test
│ ├── jest-setup.js
│ ├── unit
│ │ ├── __snapshots__
│ │ │ ├── button.spec.js.snap
│ │ │ ├── memoItem.spec.js.snap
│ │ │ └── memoInputArea.spec.js.snap
│ │ ├── dom.spec.js
│ │ ├── memoItem.spec.js
│ │ ├── memoInputArea.spec.js
│ │ └── button.spec.js
│ ├── integration
│ │ ├── __snapshots__
│ │ │ └── memoList.spec.js.snap
│ │ └── memoList.spec.js
│ ├── __snapshots__
│ │ └── snapshotMethod.spec.js.snap
│ └── snapshotMethod.spec.js
├── cypress.json
├── src
│ ├── util
│ │ └── dom.js
│ ├── index.js
│ ├── constant
│ │ └── errorCode.js
│ ├── stories
│ │ ├── memoInputArea.stories.js
│ │ ├── memoItem.stories.js
│ │ ├── memo.stories.js
│ │ └── button.stories.js
│ ├── memo.js
│ ├── memoList.js
│ ├── button.js
│ ├── memoItem.js
│ └── memoInputArea.js
├── babel.config.js
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── support
│ │ ├── index.js
│ │ └── commands.js
│ ├── plugins
│ │ └── index.js
│ └── integration
│ │ └── memo.spec.js
├── index.html
├── jest.config.js
├── webpack.config.js
├── Readme.md
└── package.json
├── chapter10-board
├── src
│ ├── image
│ │ └── spinner.gif
│ ├── js
│ │ ├── spinner.js
│ │ └── main.js
│ └── css
│ │ └── main.css
├── README.md
├── data
│ ├── latest.json
│ └── top.json
└── index.html
├── chapter12-debug
├── js
│ ├── index.js
│ ├── card.js
│ ├── modal.js
│ ├── response.js
│ └── frameworkCard.js
├── webpack.config.js
├── package.json
├── index.html
├── Readme.md
└── css
│ └── style.css
├── README.md
└── .gitignore
/chapter11-tools/npm/myScript.js:
--------------------------------------------------------------------------------
1 | console.log('hello javascript');
--------------------------------------------------------------------------------
/chapter11-tools/babel/myScript.js:
--------------------------------------------------------------------------------
1 | console.log('hello javascript');
--------------------------------------------------------------------------------
/chapter11-tools/bundler/myScript.js:
--------------------------------------------------------------------------------
1 | console.log('hello javascript');
--------------------------------------------------------------------------------
/chapter11-tools/lint/myScript.js:
--------------------------------------------------------------------------------
1 | console.log('hello javascript');
--------------------------------------------------------------------------------
/chapter11-tools/sass/myScript.js:
--------------------------------------------------------------------------------
1 | console.log('hello javascript');
--------------------------------------------------------------------------------
/chapter13-test/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {};
2 |
--------------------------------------------------------------------------------
/chapter13-test/test/jest-setup.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom";
2 |
--------------------------------------------------------------------------------
/chapter13-test/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:8000",
3 | "video": false
4 | }
5 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @use 'vars';
2 | @use 'spinner';
3 | @use 'flex';
4 | @use 'base';
5 | @use 'main';
6 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @use 'vars';
2 | @use 'spinner';
3 | @use 'flex';
4 | @use 'base';
5 | @use 'main';
6 |
--------------------------------------------------------------------------------
/chapter13-test/src/util/dom.js:
--------------------------------------------------------------------------------
1 | export function addClass(el, ...classNames) {
2 | el.classList.add(...classNames);
3 | }
4 |
--------------------------------------------------------------------------------
/chapter10-board/src/image/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bjpublic/Front-end/master/chapter10-board/src/image/spinner.gif
--------------------------------------------------------------------------------
/chapter11-tools/npm/src/image/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bjpublic/Front-end/master/chapter11-tools/npm/src/image/spinner.gif
--------------------------------------------------------------------------------
/chapter11-tools/babel/src/image/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bjpublic/Front-end/master/chapter11-tools/babel/src/image/spinner.gif
--------------------------------------------------------------------------------
/chapter11-tools/lint/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "printWidth": 100,
4 | "tabWidth": 2,
5 | "useTabs": false
6 | }
7 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/image/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bjpublic/Front-end/master/chapter11-tools/lint/src/image/spinner.gif
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/image/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bjpublic/Front-end/master/chapter11-tools/sass/src/image/spinner.gif
--------------------------------------------------------------------------------
/chapter11-tools/bundler/src/image/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bjpublic/Front-end/master/chapter11-tools/bundler/src/image/spinner.gif
--------------------------------------------------------------------------------
/chapter11-tools/lint/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@babel/preset-env", {
3 | "useBuiltIns": "usage",
4 | "corejs":3
5 | }]]
6 | }
7 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@babel/preset-env", {
3 | "useBuiltIns": "usage",
4 | "corejs":3
5 | }]]
6 | }
7 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["@babel/preset-env", {
3 | "useBuiltIns": "usage",
4 | "corejs":3
5 | }]]
6 | }
7 |
--------------------------------------------------------------------------------
/chapter13-test/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/preset-env'],
3 | plugins: [['@babel/plugin-proposal-class-properties', { loose: true }]],
4 | };
--------------------------------------------------------------------------------
/chapter13-test/test/unit/__snapshots__/button.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`button 컴포넌트 button 요소는 스냅샷과 일치해야 한다. 1`] = ``;
4 |
--------------------------------------------------------------------------------
/chapter13-test/src/index.js:
--------------------------------------------------------------------------------
1 | import "bootstrap/dist/css/bootstrap.min.css";
2 | import Memo from "./memo";
3 |
4 | const app = document.getElementById("app");
5 | const memo = new Memo(app);
6 |
--------------------------------------------------------------------------------
/chapter13-test/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
3 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
4 | };
5 |
--------------------------------------------------------------------------------
/chapter13-test/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/chapter13-test/test/integration/__snapshots__/memoList.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MemoList memoList 요소는 스냅샷과 일치해야 한다. 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/scss/_vars.scss:
--------------------------------------------------------------------------------
1 | $border: 1px solid #5a6268;
2 | $color: #faebd7;
3 | $padding: 10px;
4 | $margin: 10px;
5 |
6 | $tablet-width: 768px;
7 |
8 | $mobile-width: 375px;
9 | $mobile-font-basic: 13px;
10 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/scss/_vars.scss:
--------------------------------------------------------------------------------
1 | $border: 1px solid #5a6268;
2 | $color: #faebd7;
3 | $padding: 10px;
4 | $margin: 10px;
5 |
6 | $tablet-width: 768px;
7 |
8 | $mobile-width: 375px;
9 | $mobile-font-basic: 13px;
10 |
--------------------------------------------------------------------------------
/chapter13-test/test/__snapshots__/snapshotMethod.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`snapshot 메서드 테스트 toMatchSnapshot()은 파일 외부에 스냅샷을 기록한다. 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/chapter13-test/src/constant/errorCode.js:
--------------------------------------------------------------------------------
1 | export const errorCode = {
2 | TOO_LONG: "TOO_LONG",
3 | TOO_SHORT: "TOO_SHORT",
4 | };
5 |
6 | export const errorMessage = {
7 | TOO_LONG: "글은 50자 이하로 입력해야 합니다.",
8 | TOO_SHORT: "글을 작성해주세요!",
9 | };
10 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/scss/_spinner.scss:
--------------------------------------------------------------------------------
1 | @use 'flex';
2 |
3 | .spinner-area {
4 | @include flex.flexbox;
5 | @include flex.justify-content(center);
6 | @include flex.align-items(center);
7 |
8 | height: 400px;
9 |
10 | img {
11 | width: 50px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/webpack.config.js:
--------------------------------------------------------------------------------
1 | const developmentConfig = require('./webpack.dev.config');
2 | const productionConfig = require('./webpack.prod.config');
3 |
4 | module.exports = (env, { mode }) => {
5 | return mode === 'production' ? productionConfig : developmentConfig;
6 | };
7 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/scss/_spinner.scss:
--------------------------------------------------------------------------------
1 | @use 'flex';
2 |
3 | .spinner-area {
4 | @include flex.flexbox;
5 | @include flex.justify-content(center);
6 | @include flex.align-items(center);
7 |
8 | height: 400px;
9 |
10 | img {
11 | width: 50px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/webpack.config.js:
--------------------------------------------------------------------------------
1 | const developmentConfig = require('./webpack.dev.config');
2 | const productionConfig = require('./webpack.prod.config');
3 |
4 | module.exports = (env, { mode }) => {
5 | return mode === 'production' ? productionConfig : developmentConfig;
6 | };
7 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/webpack.config.js:
--------------------------------------------------------------------------------
1 | const developmentConfig = require('./webpack.dev.config');
2 | const productionConfig = require('./webpack.prod.config');
3 |
4 | module.exports = (env, { mode }) => {
5 | return mode === 'production' ? productionConfig : developmentConfig;
6 | };
7 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/webpack.config.js:
--------------------------------------------------------------------------------
1 | const developmentConfig = require('./webpack.dev.config');
2 | const productionConfig = require('./webpack.prod.config');
3 |
4 | module.exports = (env, { mode }) => {
5 | return mode === 'production' ? productionConfig : developmentConfig;
6 | };
7 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | es6: true,
5 | },
6 | parserOptions: {
7 | ecmaVersion: 6,
8 | sourceType: 'module',
9 | },
10 | plugins: ['prettier'],
11 | extends: ['plugin:prettier/recommended'],
12 | };
13 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/scss/_base.scss:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/scss/_base.scss:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
--------------------------------------------------------------------------------
/chapter10-board/src/js/spinner.js:
--------------------------------------------------------------------------------
1 | export function createSpinner(parent) {
2 | const spinnerAreaEl = parent.querySelector('.spinner-area');
3 | const imageEl = document.createElement('img');
4 | imageEl.alt = 'spinner';
5 | imageEl.src = './src/image/spinner.gif';
6 |
7 | spinnerAreaEl.append(imageEl);
8 | }
9 |
--------------------------------------------------------------------------------
/chapter11-tools/npm/src/js/spinner.js:
--------------------------------------------------------------------------------
1 | export function createSpinner(parent) {
2 | const spinnerAreaEl = parent.querySelector('.spinner-area');
3 | const imageEl = document.createElement('img');
4 | imageEl.alt = 'spinner';
5 | imageEl.src = './src/image/spinner.gif';
6 |
7 | spinnerAreaEl.append(imageEl);
8 | }
9 |
--------------------------------------------------------------------------------
/chapter11-tools/npm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter11-tools",
3 | "version": "1.0.0",
4 | "description": "development tools example - npm, webpack, babel, scss, lint",
5 | "scripts": {
6 | "my-script": "node ./myScript.js"
7 | },
8 | "author": "jaesung lee, jung han",
9 | "license": "ISC"
10 | }
11 |
--------------------------------------------------------------------------------
/chapter12-debug/js/index.js:
--------------------------------------------------------------------------------
1 | import { response } from "./response";
2 | import { makeFrameworkModel, renderCard, bindCardEvent } from "./card";
3 |
4 | import 'bootstrap/dist/css/bootstrap.min.css';
5 | import '../css/style.css';
6 |
7 | const frontEndFrameworkCards = makeFrameworkModel(response);
8 |
9 | renderCard(frontEndFrameworkCards);
--------------------------------------------------------------------------------
/chapter10-board/README.md:
--------------------------------------------------------------------------------
1 | # chapter10-board
2 |
3 | 10장 게시판 만들기 예제
4 |
5 | > 편의를 위해 모든 예제에서 개발 IDE는 VS Code를 사용한다고 가정합니다.
6 |
7 | ## 실행 방법
8 |
9 | VS Code의 Live Server extension를 사용하여 index.html 파일을 실행합니다.
10 |
11 | 
12 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/src/js/spinner.js:
--------------------------------------------------------------------------------
1 | import spinner from '../image/spinner.gif';
2 |
3 | export function createSpinner(parent) {
4 | const spinnerAreaEl = parent.querySelector('.spinner-area');
5 | const imageEl = document.createElement('img');
6 | imageEl.alt = 'spinner';
7 | imageEl.src = spinner;
8 |
9 | spinnerAreaEl.append(imageEl);
10 | }
--------------------------------------------------------------------------------
/chapter11-tools/babel/src/js/spinner.js:
--------------------------------------------------------------------------------
1 | import spinner from '../image/spinner.gif';
2 |
3 | export function createSpinner(parent) {
4 | const spinnerAreaEl = parent.querySelector('.spinner-area');
5 | const imageEl = document.createElement('img');
6 | imageEl.alt = 'spinner';
7 | imageEl.src = spinner;
8 |
9 | spinnerAreaEl.append(imageEl);
10 | }
11 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/js/spinner.js:
--------------------------------------------------------------------------------
1 | import spinner from '../image/spinner.gif';
2 |
3 | export function createSpinner(parent) {
4 | const spinnerAreaEl = parent.querySelector('.spinner-area');
5 | const imageEl = document.createElement('img');
6 | imageEl.alt = 'spinner';
7 | imageEl.src = spinner;
8 |
9 | spinnerAreaEl.append(imageEl);
10 | }
11 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/js/spinner.js:
--------------------------------------------------------------------------------
1 | import spinner from '../image/spinner.gif';
2 |
3 | export function createSpinner(parent) {
4 | const spinnerAreaEl = parent.querySelector('.spinner-area');
5 | const imageEl = document.createElement('img');
6 | imageEl.alt = 'spinner';
7 | imageEl.src = spinner;
8 |
9 | spinnerAreaEl.append(imageEl);
10 | }
11 |
--------------------------------------------------------------------------------
/chapter13-test/test/unit/dom.spec.js:
--------------------------------------------------------------------------------
1 | import { addClass } from "../../src/util/dom";
2 |
3 | describe("dom util", () => {
4 | it("addClass 함수는 전달된 요소에 class를 추가해야 한다.", () => {
5 | const el = document.createElement("div");
6 |
7 | addClass(el, "my-class1", "my-class2");
8 |
9 | expect(el).toHaveClass("my-class1", "my-class2");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['stylelint-scss', 'stylelint-prettier'],
3 | extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
4 | rules: {
5 | 'prettier/prettier': true,
6 | 'at-rule-no-unknown': null,
7 | 'scss/at-rule-no-unknown': true,
8 | 'no-descending-specificity': null,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/chapter12-debug/js/card.js:
--------------------------------------------------------------------------------
1 | import FrameworkCard from "./frameworkCard";
2 |
3 | export function makeFrameworkModel(res) {
4 | return res.map((frameworkInfo) => new FrameworkCard(frameworkInfo));
5 | }
6 |
7 | export function renderCard(cards) {
8 | const el = document.getElementById("card-area");
9 |
10 | cards.forEach((card) => {
11 | el.appendChild(card.makeCardElement());
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/chapter13-test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/chapter13-test/test/unit/__snapshots__/memoItem.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MemoItem memoItem 요소는 스냅샷과 일치해야 한다. 1`] = `
4 |
7 |
10 | item content
11 |
12 |
18 |
19 | `;
20 |
--------------------------------------------------------------------------------
/chapter13-test/test/snapshotMethod.spec.js:
--------------------------------------------------------------------------------
1 | function getHelloButtonHtml() {
2 | return "";
3 | }
4 |
5 | describe("snapshot 메서드 테스트", () => {
6 | it("toMatchInlineSnapshot()은 파일 내부에 스냅샷을 기록한다.", () => {
7 | expect(getHelloButtonHtml()).toMatchInlineSnapshot(`
8 |
11 | `);
12 | });
13 |
14 | it("toMatchSnapshot()은 파일 외부에 스냅샷을 기록한다.", () => {
15 | expect(getHelloButtonHtml()).toMatchSnapshot();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/chapter12-debug/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 |
3 | module.exports = {
4 | entry: './js/index.js',
5 | plugins: [new HtmlWebpackPlugin({ template: 'index.html' })],
6 | module: {
7 | rules: [
8 | {
9 | test: /\.css$/,
10 | use: ['style-loader', 'css-loader'],
11 | },
12 | ],
13 | },
14 | devtool: 'eval-source-map',
15 | devServer: {
16 | host: '0.0.0.0',
17 | port: 8000,
18 | stats: 'errors-warnings',
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/chapter13-test/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transformIgnorePatterns: ["/node_modules/"],
3 | transform: {
4 | "^.+\\.js?$": "babel-jest",
5 | },
6 | testMatch: ["**/test/**/*.spec.js"],
7 | testEnvironment: "jsdom",
8 | snapshotSerializers: ["jest-serializer-html"],
9 | watchPathIgnorePatterns: [
10 | "/.storybook",
11 | "/.stories",
12 | "/node_modules/",
13 | "/cypress",
14 | ],
15 | setupFilesAfterEnv: ["/test/jest-setup.js"],
16 | };
17 |
--------------------------------------------------------------------------------
/chapter11-tools/npm/README.md:
--------------------------------------------------------------------------------
1 | # chapter11-tools
2 |
3 | 11장 npm 환경 적용
4 |
5 | node.js 환경에서의 npm으로 의존성을 관리하는 방법과 스크립트를 실행하는 방법을 알아보는 단계입니다.
6 |
7 | > 편의를 위해 모든 예제에서 개발 IDE는 VS Code를 사용한다고 가정합니다.
8 |
9 | ## 실행 방법
10 |
11 | VS Code의 Live Server extension를 사용하여 index.html 파일을 실행합니다.
12 |
13 | 
14 |
15 | ### 커스텀 명령어 실행
16 |
17 | 아래 명령어는 `/chapter11-tools/bundler` 경로에서 실행해야 합니다.
18 |
19 | ```sh
20 | npm run my-script
21 | ```
22 |
--------------------------------------------------------------------------------
/chapter12-debug/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter12-debug",
3 | "version": "1.0.0",
4 | "description": "12장 디버깅 예제",
5 | "scripts": {
6 | "start": "webpack serve"
7 | },
8 | "author": "",
9 | "license": "ISC",
10 | "devDependencies": {
11 | "css-loader": "^5.0.1",
12 | "html-webpack-plugin": "^4.5.1",
13 | "style-loader": "^2.0.0",
14 | "webpack": "^5.17.0",
15 | "webpack-cli": "^4.4.0",
16 | "webpack-dev-server": "^3.11.2"
17 | },
18 | "dependencies": {
19 | "bootstrap": "^4.6.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/README.md:
--------------------------------------------------------------------------------
1 | # chapter11-tools
2 |
3 | 11장 bundler(webpack) 적용
4 |
5 | webpack 번들러를 적용하여 개발 서버를 띄우고 빌드하는 방법을 알아보는 단계입니다.
6 |
7 | > 편의를 위해 모든 예제에서 개발 IDE는 VS Code를 사용한다고 가정합니다.
8 |
9 | ## 실행 방법
10 |
11 | 아래 명령어는 `/chapter11-tools/bundler` 경로에서 실행해야 합니다.
12 |
13 | ### 의존성 설치
14 |
15 | ```sh
16 | npm ci
17 | // or
18 | npm install
19 | ```
20 |
21 | ### 개발 서버 실행
22 |
23 | ```sh
24 | npm run serve
25 | ```
26 |
27 | ### 빌드
28 |
29 | 1. 개발 모드 빌드
30 |
31 | ```sh
32 | npm run build:dev
33 | ```
34 |
35 | 2. 프로덕션 모드 빌드
36 |
37 | ```sh
38 | npm run build:prod
39 | ```
40 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/README.md:
--------------------------------------------------------------------------------
1 | # chapter11-tools
2 |
3 | 11장 babel 적용
4 |
5 | babel을 적용하여 트랜스파일하는 방법을 알아보는 단계입니다.
6 |
7 | > 편의를 위해 모든 예제에서 개발 IDE는 VS Code를 사용한다고 가정합니다.
8 |
9 | ## 실행 방법
10 |
11 | 아래 명령어는 `/chapter11-tools/babel` 경로에서 실행해야 합니다.
12 |
13 | ### 의존성 설치
14 |
15 | ```sh
16 | npm ci
17 | // or
18 | npm install
19 | ```
20 |
21 | ### 개발 서버 실행
22 |
23 | ```sh
24 | npm run serve
25 | ```
26 |
27 | ### 빌드
28 |
29 | 1. 개발 모드 빌드
30 |
31 | ```sh
32 | npm run build:dev
33 | ```
34 |
35 | 2. 프로덕션 모드 빌드
36 |
37 | ```sh
38 | npm run build:prod
39 | ```
40 |
41 | ### 예제 트랜스파일
42 |
43 | ```sh
44 | npm run transpile
45 | ```
46 |
--------------------------------------------------------------------------------
/chapter13-test/src/stories/memoInputArea.stories.js:
--------------------------------------------------------------------------------
1 | import MemoInputArea from "../memoInputArea";
2 | import { MINIMAL_VIEWPORTS } from "@storybook/addon-viewport";
3 | import "bootstrap/dist/css/bootstrap.min.css";
4 |
5 | export default {
6 | title: "MemoInputArea Component",
7 | viewport: {
8 | viewports: MINIMAL_VIEWPORTS,
9 | },
10 | };
11 |
12 | function createContainer() {
13 | const el = document.createElement("div");
14 | el.style.width = "100%";
15 |
16 | return el;
17 | }
18 |
19 | export const basic = () => {
20 | const el = createContainer();
21 | const memoInputArea = new MemoInputArea(el, { addMemoItem: () => {} });
22 |
23 | return el;
24 | };
25 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 |
3 | module.exports = {
4 | mode: 'development',
5 | entry: './src/js/main.js',
6 | plugins: [new HtmlWebpackPlugin({ template: 'index.html' })],
7 | module: {
8 | rules: [
9 | {
10 | test: /\.css$/,
11 | use: ['style-loader', 'css-loader'],
12 | },
13 | {
14 | test: /\.(png|jpg|gif)$/i,
15 | use: ['file-loader'],
16 | },
17 | ],
18 | },
19 | devtool: 'eval-source-map',
20 | devServer: {
21 | host: '0.0.0.0',
22 | port: 8000,
23 | stats: 'errors-warnings',
24 | open: true,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/chapter13-test/test/unit/__snapshots__/memoInputArea.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`memoInputArea MemoInputArea 요소는 스냅샷과 일치해야 한다. 1`] = `
4 |
7 |
10 |
15 |
20 |
21 |
24 |
29 |
30 |
31 | `;
32 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/README.md:
--------------------------------------------------------------------------------
1 | # chapter11-tools
2 |
3 | 11장 sass 적용
4 |
5 | sass을 적용하여 트랜스파일하는 방법을 알아보는 단계입니다.
6 |
7 | > 편의를 위해 모든 예제에서 개발 IDE는 VS Code를 사용한다고 가정합니다.
8 |
9 | ## 실행 방법
10 |
11 | 아래 명령어는 `/chapter11-tools/sass` 경로에서 실행해야 합니다.
12 |
13 | ### 의존성 설치
14 |
15 | ```sh
16 | npm ci
17 | // or
18 | npm install
19 | ```
20 |
21 | ### 개발 서버 실행
22 |
23 | ```sh
24 | npm run serve
25 | ```
26 |
27 | ### 빌드
28 |
29 | 1. 개발 모드 빌드
30 |
31 | ```sh
32 | npm run build:dev
33 | ```
34 |
35 | 2. 프로덕션 모드 빌드
36 |
37 | ```sh
38 | npm run build:prod
39 | ```
40 |
41 | ### 예제 트랜스파일
42 |
43 | ```sh
44 | npm run transpile
45 | ```
46 |
47 | ### 예제 sass 트랜스파일
48 |
49 | ```sh
50 | npm run transpile:sass
51 | ```
52 |
--------------------------------------------------------------------------------
/chapter13-test/src/stories/memoItem.stories.js:
--------------------------------------------------------------------------------
1 | import MemoList from "../memoList";
2 | import { MINIMAL_VIEWPORTS } from "@storybook/addon-viewport";
3 | import "bootstrap/dist/css/bootstrap.min.css";
4 |
5 | export default {
6 | title: "MemoItem Component",
7 | viewport: {
8 | viewports: MINIMAL_VIEWPORTS,
9 | },
10 | };
11 |
12 | function createContainer() {
13 | const el = document.createElement("div");
14 | el.style.width = "350px";
15 |
16 | return el;
17 | }
18 |
19 | export const memoItemWithContent = () => {
20 | const el = createContainer();
21 | const memoList = new MemoList(el);
22 |
23 | memoList.addMemoItem("Lorem ipsum is placeholder text");
24 |
25 | return el;
26 | };
27 |
--------------------------------------------------------------------------------
/chapter13-test/test/unit/memoItem.spec.js:
--------------------------------------------------------------------------------
1 | import MemoItem from "../../src/memoItem";
2 |
3 | describe("MemoItem", () => {
4 | let memoItem, el;
5 |
6 | beforeEach(() => {
7 | el = document.createElement("div");
8 | const content = "item content";
9 |
10 | memoItem = new MemoItem(el, { content });
11 | });
12 |
13 | it("memoItem 요소가 렌더링된다.", () => {
14 | expect(el).toContainElement(memoItem.itemEl);
15 | });
16 |
17 | it("memoItem 요소는 스냅샷과 일치해야 한다.", () => {
18 | expect(memoItem.itemEl).toMatchSnapshot();
19 | });
20 |
21 | it("destroy() 메서드 호출 시 삭제된다.", () => {
22 | memoItem.destroy();
23 |
24 | expect(el).not.toContainElement(memoItem.itemEl);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/chapter12-debug/js/modal.js:
--------------------------------------------------------------------------------
1 | const modalLayer = document.querySelector("#modal-layer");
2 | const modalCloseButton = document.querySelector("#modal-close");
3 |
4 | export function showModal({ name, desc, website }) {
5 | const infoArea = document.querySelector("#modal-info");
6 |
7 | infoArea.innerHTML = `
8 | ${name}
9 | ${desc}
10 | go website
11 | `;
12 | modalLayer.classList.remove("hidden");
13 | modalLayer.classList.add("visible");
14 | }
15 |
16 | export function hideModal() {
17 | modalLayer.classList.remove("visible");
18 | modalLayer.classList.add("hidden");
19 | }
20 |
21 | modalCloseButton.addEventListener("click", hideModal);
--------------------------------------------------------------------------------
/chapter13-test/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import "./commands";
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/chapter13-test/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require("html-webpack-plugin");
2 |
3 | module.exports = {
4 | entry: "./src/index.js",
5 | plugins: [new HtmlWebpackPlugin({ template: "index.html" })],
6 | module: {
7 | rules: [
8 | {
9 | test: /\.css$/,
10 | use: ["style-loader", "css-loader"],
11 | },
12 | {
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | loader: "babel-loader",
16 | options: {
17 | presets: [
18 | "@babel/preset-env",
19 | { plugins: ["@babel/plugin-proposal-class-properties"] },
20 | ],
21 | },
22 | },
23 | ],
24 | },
25 | devtool: "eval-source-map",
26 | devServer: {
27 | host: "0.0.0.0",
28 | port: 8000,
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/README.md:
--------------------------------------------------------------------------------
1 | # chapter11-tools
2 |
3 | 11장 lint, prettier 적용
4 |
5 | lint, prettier를 적용하여 프로젝트 파일의 포맷을 일관성있게 통일하는 방법을 알아보는 단계입니다.
6 |
7 | > 편의를 위해 모든 예제에서 개발 IDE는 VS Code를 사용한다고 가정합니다.
8 |
9 | ## 실행 방법
10 |
11 | 아래 명령어는 `/chapter11-tools/lint` 경로에서 실행해야 합니다.
12 |
13 | ### 의존성 설치
14 |
15 | ```sh
16 | npm ci
17 | // or
18 | npm install
19 | ```
20 |
21 | ### 개발 서버 실행
22 |
23 | ```sh
24 | npm run serve
25 | ```
26 |
27 | ### 빌드
28 |
29 | 1. 개발 모드 빌드
30 |
31 | ```sh
32 | npm run build:dev
33 | ```
34 |
35 | 2. 프로덕션 모드 빌드
36 |
37 | ```sh
38 | npm run build:prod
39 | ```
40 |
41 | ### 예제 트랜스파일
42 |
43 | ```sh
44 | npm run transpile
45 | ```
46 |
47 | ### 예제 sass 트랜스파일
48 |
49 | ```sh
50 | npm run transpile:sass
51 | ```
52 |
53 | ### prettier 실행
54 |
55 | ```sh
56 | npm run prettify
57 | ```
--------------------------------------------------------------------------------
/chapter13-test/src/stories/memo.stories.js:
--------------------------------------------------------------------------------
1 | import Memo from "../memo";
2 | import { MINIMAL_VIEWPORTS } from "@storybook/addon-viewport";
3 | import "bootstrap/dist/css/bootstrap.min.css";
4 |
5 | export default {
6 | title: "Memo App",
7 | viewport: {
8 | viewports: MINIMAL_VIEWPORTS,
9 | },
10 | };
11 |
12 | function createContainer() {
13 | const el = document.createElement("div");
14 | el.style.width = "100%";
15 |
16 | return el;
17 | }
18 |
19 | export const emptyMemoList = () => {
20 | const el = createContainer();
21 | const memo = new Memo(el);
22 |
23 | return el;
24 | };
25 |
26 | export const withTwoMemoList = () => {
27 | const el = createContainer();
28 | const memo = new Memo(el);
29 | memo.addMemoItem("첫번째 메모입니다.");
30 | memo.addMemoItem("두번째 메모입니다.");
31 |
32 | return el;
33 | };
34 |
--------------------------------------------------------------------------------
/chapter13-test/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | };
22 |
--------------------------------------------------------------------------------
/chapter12-debug/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | front end framework
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Front-End Framework
13 |
16 |
17 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 프런트엔드 개념 잡기 - 기초부터 완성까지, 프런트엔드
2 |
3 | 각 예제의 단계는 README를 읽어주세요.
4 |
5 | - 10장 뉴스 게시판 만들기
6 | - 링크: https://github.com/bjpublic/Front-end/tree/master/chapter10-board
7 | - 11장 개발 도구 예제
8 | - 링크
9 | - npm: https://github.com/bjpublic/Front-end/tree/master/chapter11-tools/npm
10 | - bundler: https://github.com/bjpublic/Front-end/tree/master/chapter11-tools/bundler
11 | - babel: https://github.com/bjpublic/Front-end/tree/master/chapter11-tools/babel
12 | - sass: https://github.com/bjpublic/Front-end/tree/master/chapter11-tools/sass
13 | - lint: https://github.com/bjpublic/Front-end/tree/master/chapter11-tools/lint
14 | - 12장 디버깅 예제
15 | - 링크: https://github.com/bjpublic/Front-end/tree/master/chapter12-debug
16 | - 13, 14장 테스트 예제
17 | - 링크: https://github.com/bjpublic/Front-end/tree/master/chapter13-test
18 |
19 | ```
20 | ⚠️ 잘못된 예제 코드 혹은 책 내용에 문제가 있는 경우 이슈를 이용해서 제보 부탁드립니다!
21 | ```
22 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/src/js/polyfill/append.js:
--------------------------------------------------------------------------------
1 | // Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
2 | (function (arr) {
3 | arr.forEach(function (item) {
4 | if (item.hasOwnProperty('append')) {
5 | return;
6 | }
7 | Object.defineProperty(item, 'append', {
8 | configurable: true,
9 | enumerable: true,
10 | writable: true,
11 | value: function append() {
12 | var argArr = Array.prototype.slice.call(arguments),
13 | docFrag = document.createDocumentFragment();
14 |
15 | argArr.forEach(function (argItem) {
16 | var isNode = argItem instanceof Node;
17 | docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
18 | });
19 |
20 | this.appendChild(docFrag);
21 | },
22 | });
23 | });
24 | })([Element.prototype, Document.prototype, DocumentFragment.prototype]);
25 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/js/polyfill/append.js:
--------------------------------------------------------------------------------
1 | // Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
2 | (function (arr) {
3 | arr.forEach(function (item) {
4 | if (item.hasOwnProperty('append')) {
5 | return;
6 | }
7 | Object.defineProperty(item, 'append', {
8 | configurable: true,
9 | enumerable: true,
10 | writable: true,
11 | value: function append() {
12 | var argArr = Array.prototype.slice.call(arguments),
13 | docFrag = document.createDocumentFragment();
14 |
15 | argArr.forEach(function (argItem) {
16 | var isNode = argItem instanceof Node;
17 | docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
18 | });
19 |
20 | this.appendChild(docFrag);
21 | },
22 | });
23 | });
24 | })([Element.prototype, Document.prototype, DocumentFragment.prototype]);
25 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/js/polyfill/append.js:
--------------------------------------------------------------------------------
1 | // Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
2 | (function (arr) {
3 | arr.forEach(function (item) {
4 | if (item.hasOwnProperty('append')) {
5 | return;
6 | }
7 | Object.defineProperty(item, 'append', {
8 | configurable: true,
9 | enumerable: true,
10 | writable: true,
11 | value: function append() {
12 | var argArr = Array.prototype.slice.call(arguments),
13 | docFrag = document.createDocumentFragment();
14 |
15 | argArr.forEach(function (argItem) {
16 | var isNode = argItem instanceof Node;
17 | docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
18 | });
19 |
20 | this.appendChild(docFrag);
21 | },
22 | });
23 | });
24 | })([Element.prototype, Document.prototype, DocumentFragment.prototype]);
25 |
--------------------------------------------------------------------------------
/chapter13-test/src/memo.js:
--------------------------------------------------------------------------------
1 | import { errorCode, errorMessage } from "./constant/errorCode";
2 | import MemoList from "./memoList";
3 | import MemoInputArea from "./memoInputArea";
4 |
5 | function validate(contents) {
6 | if (contents.length <= 0) {
7 | return errorCode.TOO_SHORT;
8 | } else if (contents.length >= 50) {
9 | return errorCode.TOO_LONG;
10 | }
11 |
12 | return false;
13 | }
14 |
15 | export default class Memo {
16 | constructor(el) {
17 | this.el = el;
18 | this.render();
19 | }
20 |
21 | render() {
22 | this.memoInputArea = new MemoInputArea(this.el, {
23 | addMemoItem: this.addMemoItem,
24 | });
25 | this.memoList = new MemoList(this.el);
26 | }
27 |
28 | addMemoItem = (content) => {
29 | const errorCode = validate(content);
30 |
31 | if (errorCode) {
32 | alert(errorMessage[errorCode]);
33 | return;
34 | }
35 |
36 | this.memoList.addMemoItem(content);
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter11-tools",
3 | "version": "1.0.0",
4 | "description": "development tools example - npm, webpack, babel, scss, lint",
5 | "scripts": {
6 | "my-script": "node ./myScript.js",
7 | "serve": "webpack serve",
8 | "build:dev": "webpack --config webpack.config.js --mode=development",
9 | "build:prod": "webpack --config webpack.config.js --mode=production"
10 | },
11 | "author": "jaesung lee, jung han",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "clean-webpack-plugin": "^3.0.0",
15 | "css-loader": "^5.0.1",
16 | "css-minimizer-webpack-plugin": "^1.1.5",
17 | "file-loader": "^6.2.0",
18 | "html-webpack-plugin": "^4.5.0",
19 | "mini-css-extract-plugin": "^1.3.3",
20 | "optimize-css-assets-webpack-plugin": "^5.0.4",
21 | "style-loader": "^2.0.0",
22 | "webpack": "^5.10.0",
23 | "webpack-cli": "^4.3.0",
24 | "webpack-dev-server": "^3.11.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/js/main.js',
7 | output: {
8 | environment: {
9 | arrowFunction: false,
10 | },
11 | },
12 | plugins: [new HtmlWebpackPlugin({ template: 'index.html' })],
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | use: ['style-loader', 'css-loader'],
18 | },
19 | {
20 | test: /\.(png|jpg|gif)$/i,
21 | use: ['file-loader'],
22 | },
23 | {
24 | test: /\.js$/,
25 | include: [path.resolve(__dirname, 'src/js')],
26 | exclude: /node_modules/,
27 | use: { loader: 'babel-loader' },
28 | },
29 | ],
30 | },
31 | devtool: 'eval-source-map',
32 | devServer: {
33 | host: '0.0.0.0',
34 | port: 8000,
35 | stats: 'errors-warnings',
36 | open: true,
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/chapter13-test/src/memoList.js:
--------------------------------------------------------------------------------
1 | import MemoItem from "./memoItem";
2 | import { addClass } from "./util/dom";
3 |
4 | export default class MemoList {
5 | constructor(el) {
6 | this.el = el;
7 | this.memoItems = [];
8 | this.render();
9 | }
10 |
11 | render() {
12 | const list = document.createElement("ul");
13 |
14 | addClass(list, "list-group");
15 | list.addEventListener("click", this.removeMemoItem);
16 |
17 | this.list = list;
18 |
19 | this.el.append(list);
20 | }
21 |
22 | addMemoItem(content) {
23 | this.memoItems.push(new MemoItem(this.list, { content }));
24 | }
25 |
26 | removeMemoItem = (ev) => {
27 | const { itemId } = ev.target.dataset;
28 |
29 | if (itemId) {
30 | const index = this.memoItems.findIndex(
31 | (item) => item.id === Number(itemId)
32 | );
33 |
34 | if (index !== -1) {
35 | this.memoItems[index].destroy();
36 | this.memoItems.splice(index, 1);
37 | }
38 | }
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/chapter13-test/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/cypress/add-commands";
2 | // ***********************************************
3 | // This example commands.js shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add("login", (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
27 |
--------------------------------------------------------------------------------
/chapter13-test/src/stories/button.stories.js:
--------------------------------------------------------------------------------
1 | import Button from "../button";
2 | import "bootstrap/dist/css/bootstrap.min.css";
3 |
4 | export default {
5 | title: "Button Component",
6 | argTypes: {
7 | classNames: { control: "array" },
8 | textContent: { control: "text" },
9 | attributes: { control: "object" },
10 | },
11 | };
12 |
13 | function createContainer() {
14 | const el = document.createElement("div");
15 | el.style.width = "100%";
16 |
17 | return el;
18 | }
19 |
20 | const Template = (args) => {
21 | const el = createContainer();
22 | const button = new Button(el, { ...args });
23 |
24 | return el;
25 | };
26 |
27 | export const addMemoButton = Template.bind({});
28 | addMemoButton.args = {
29 | classNames: ["btn", "btn-primary"],
30 | textContent: "Add Memo",
31 | attributes: { id: "1" },
32 | };
33 |
34 | export const deleteMemoButton = Template.bind({});
35 | deleteMemoButton.args = {
36 | classNames: ["btn", "btn-danger", "btn-sm"],
37 | textContent: "X",
38 | attributes: { "data-item-id": "btn-id-1" },
39 | };
40 |
--------------------------------------------------------------------------------
/chapter13-test/src/button.js:
--------------------------------------------------------------------------------
1 | import { addClass } from "./util/dom";
2 |
3 | export default class Button {
4 | constructor(el, props = {}) {
5 | this.el = el;
6 | this.props = props;
7 | this.render();
8 | }
9 |
10 | render() {
11 | const button = document.createElement("button");
12 | const { classNames, attributes, textContent } = this.props;
13 |
14 | if (classNames) {
15 | addClass(button, ...classNames);
16 | }
17 |
18 | if (attributes) {
19 | Object.keys(attributes).forEach((attrName) => {
20 | button.setAttribute(attrName, attributes[attrName]);
21 | });
22 | }
23 |
24 | button.textContent = textContent ?? "";
25 |
26 | Object.keys(this.props)
27 | .filter((propName) => /^on[A-Z]/.test(propName))
28 | .forEach((propName) => {
29 | const eventName = propName.replace(/^on/, "").toLowerCase();
30 |
31 | button.addEventListener(eventName, this.props[propName]);
32 | });
33 |
34 | this.buttonEl = button;
35 |
36 | this.el.appendChild(button);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/chapter12-debug/js/response.js:
--------------------------------------------------------------------------------
1 | export const response = [
2 | {
3 | name: "React.js",
4 | index: 0,
5 | desc:
6 | "React는 사용자 인터페이스를 만들기 위한 JavaScript 라이브러리입니다. React는 상호작용이 많은 UI를 만들 때 생기는 어려움을 줄여줍니다.",
7 | website: "https://reactjs.org/",
8 | imageUrl:
9 | "https://user-images.githubusercontent.com/35371660/105100538-b4637580-5af0-11eb-9f20-aafa0e295fd8.png",
10 | },
11 | {
12 | name: "vue.js",
13 | index: 1,
14 | desc:
15 | "Vue는 사용자 인터페이스를 만들기 위한 프로그레시브 프레임워크입니다. 다른 단일형 프레임워크와 달리 Vue는 점진적으로 채택할 수 있도록 설계하였습니다.",
16 | website: "https://v3.vuejs.org/",
17 | imageUrl:
18 | "https://user-images.githubusercontent.com/35371660/105100534-b3324880-5af0-11eb-9ce6-6e71fd1628de.png"
19 | },
20 | {
21 | name: "Svelte",
22 | // index: 2,
23 | desc:
24 | "Svelte는 앱을 실행 시점(Run time)에 해석하는 것이 아니라 빌드 시점(Build time)에 JavaScript로 변환합니다.",
25 | website: "https://svelte.dev/",
26 | imageUrl:
27 | "https://user-images.githubusercontent.com/35371660/105100693-03110f80-5af1-11eb-9727-8a41af7eb725.png"
28 | }
29 | ];
--------------------------------------------------------------------------------
/chapter11-tools/lint/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/js/main.js',
7 | output: {
8 | environment: {
9 | arrowFunction: false,
10 | },
11 | },
12 | plugins: [new HtmlWebpackPlugin({ template: 'index.html' })],
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | use: ['style-loader', 'css-loader'],
18 | },
19 | {
20 | test: /\.(png|jpg|gif)$/i,
21 | use: ['file-loader'],
22 | },
23 | {
24 | test: /\.js$/,
25 | include: [path.resolve(__dirname, 'src/js')],
26 | exclude: /node_modules/,
27 | use: { loader: 'babel-loader' },
28 | },
29 | {
30 | test: /\.scss$/i,
31 | use: ['style-loader', 'css-loader', 'sass-loader'],
32 | },
33 | ],
34 | },
35 | devtool: 'eval-source-map',
36 | devServer: {
37 | host: '0.0.0.0',
38 | port: 8000,
39 | stats: 'errors-warnings',
40 | open: true
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/js/main.js',
7 | output: {
8 | environment: {
9 | arrowFunction: false,
10 | },
11 | },
12 | plugins: [new HtmlWebpackPlugin({ template: 'index.html' })],
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | use: ['style-loader', 'css-loader'],
18 | },
19 | {
20 | test: /\.(png|jpg|gif)$/i,
21 | use: ['file-loader'],
22 | },
23 | {
24 | test: /\.js$/,
25 | include: [path.resolve(__dirname, 'src/js')],
26 | exclude: /node_modules/,
27 | use: { loader: 'babel-loader' },
28 | },
29 | {
30 | test: /\.scss$/i,
31 | use: ['style-loader', 'css-loader', 'sass-loader'],
32 | },
33 | ],
34 | },
35 | devtool: 'eval-source-map',
36 | devServer: {
37 | host: '0.0.0.0',
38 | port: 8000,
39 | stats: 'errors-warnings',
40 | open: true,
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/chapter10-board/data/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "CSS 중앙 정렬하기",
5 | "summary": "여러가지 테스트를 통해 5가지 중앙 정렬 방법 중 어떤 것이 가장 변화에 탄력적으로 대응하는지 확인해보자.",
6 | "link": "https://ui.toast.com/weekly-pick/ko_20201222"
7 | },
8 | {
9 | "title": "LCP(Largest Contentful Paint) 최적화하기",
10 | "summary": "Largest Contentful Paint(LCP)는 Core Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정한다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.",
11 | "link": "https://ui.toast.com/weekly-pick/ko_202012101720"
12 | },
13 | {
14 | "title": "useEffect를 테스트 하는 방법",
15 | "summary": "React.useEffect 코드를 사용하다 보면 도대체 이것들을 어떻게 테스트해야 할까 하는 궁금증이 생길 수 있다. 그리고 꽤 자주 묻곤 하는 질문이기도 하다. 이에 대한 대답은 다소 황당할 수 있겠지만 당연한 내용이다. 무언가 테스트를 할 때는 아래와 같은 의식의 흐름으로 진행한다.",
16 | "link": "https://ui.toast.com/weekly-pick/ko_20201126"
17 | },
18 | {
19 | "title": "Async Cookie Store API 살펴보기",
20 | "summary": "document.cookie를 통해 쿠키를 가져오는 이상한 방법에 지쳤는가? 쿠키가 실제로 저장되었는지 여부를 알 수 없는 것이 싫었는가? 그렇다면, 크롬 87버전부터 사용 가능한 새로운 Cookie Store API에 대해 살펴볼 필요가 있다.",
21 | "link": "https://ui.toast.com/weekly-pick/ko_20201027"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/data/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "CSS 중앙 정렬하기",
5 | "summary": "여러가지 테스트를 통해 5가지 중앙 정렬 방법 중 어떤 것이 가장 변화에 탄력적으로 대응하는지 확인해보자.",
6 | "link": "https://ui.toast.com/weekly-pick/ko_20201222"
7 | },
8 | {
9 | "title": "LCP(Largest Contentful Paint) 최적화하기",
10 | "summary": "Largest Contentful Paint(LCP)는 Core Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정한다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.",
11 | "link": "https://ui.toast.com/weekly-pick/ko_202012101720"
12 | },
13 | {
14 | "title": "useEffect를 테스트 하는 방법",
15 | "summary": "React.useEffect 코드를 사용하다 보면 도대체 이것들을 어떻게 테스트해야 할까 하는 궁금증이 생길 수 있다. 그리고 꽤 자주 묻곤 하는 질문이기도 하다. 이에 대한 대답은 다소 황당할 수 있겠지만 당연한 내용이다. 무언가 테스트를 할 때는 아래와 같은 의식의 흐름으로 진행한다.",
16 | "link": "https://ui.toast.com/weekly-pick/ko_20201126"
17 | },
18 | {
19 | "title": "Async Cookie Store API 살펴보기",
20 | "summary": "document.cookie를 통해 쿠키를 가져오는 이상한 방법에 지쳤는가? 쿠키가 실제로 저장되었는지 여부를 알 수 없는 것이 싫었는가? 그렇다면, 크롬 87버전부터 사용 가능한 새로운 Cookie Store API에 대해 살펴볼 필요가 있다.",
21 | "link": "https://ui.toast.com/weekly-pick/ko_20201027"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/chapter11-tools/lint/data/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "CSS 중앙 정렬하기",
5 | "summary": "여러가지 테스트를 통해 5가지 중앙 정렬 방법 중 어떤 것이 가장 변화에 탄력적으로 대응하는지 확인해보자.",
6 | "link": "https://ui.toast.com/weekly-pick/ko_20201222"
7 | },
8 | {
9 | "title": "LCP(Largest Contentful Paint) 최적화하기",
10 | "summary": "Largest Contentful Paint(LCP)는 Core Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정한다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.",
11 | "link": "https://ui.toast.com/weekly-pick/ko_202012101720"
12 | },
13 | {
14 | "title": "useEffect를 테스트 하는 방법",
15 | "summary": "React.useEffect 코드를 사용하다 보면 도대체 이것들을 어떻게 테스트해야 할까 하는 궁금증이 생길 수 있다. 그리고 꽤 자주 묻곤 하는 질문이기도 하다. 이에 대한 대답은 다소 황당할 수 있겠지만 당연한 내용이다. 무언가 테스트를 할 때는 아래와 같은 의식의 흐름으로 진행한다.",
16 | "link": "https://ui.toast.com/weekly-pick/ko_20201126"
17 | },
18 | {
19 | "title": "Async Cookie Store API 살펴보기",
20 | "summary": "document.cookie를 통해 쿠키를 가져오는 이상한 방법에 지쳤는가? 쿠키가 실제로 저장되었는지 여부를 알 수 없는 것이 싫었는가? 그렇다면, 크롬 87버전부터 사용 가능한 새로운 Cookie Store API에 대해 살펴볼 필요가 있다.",
21 | "link": "https://ui.toast.com/weekly-pick/ko_20201027"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/chapter11-tools/npm/data/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "CSS 중앙 정렬하기",
5 | "summary": "여러가지 테스트를 통해 5가지 중앙 정렬 방법 중 어떤 것이 가장 변화에 탄력적으로 대응하는지 확인해보자.",
6 | "link": "https://ui.toast.com/weekly-pick/ko_20201222"
7 | },
8 | {
9 | "title": "LCP(Largest Contentful Paint) 최적화하기",
10 | "summary": "Largest Contentful Paint(LCP)는 Core Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정한다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.",
11 | "link": "https://ui.toast.com/weekly-pick/ko_202012101720"
12 | },
13 | {
14 | "title": "useEffect를 테스트 하는 방법",
15 | "summary": "React.useEffect 코드를 사용하다 보면 도대체 이것들을 어떻게 테스트해야 할까 하는 궁금증이 생길 수 있다. 그리고 꽤 자주 묻곤 하는 질문이기도 하다. 이에 대한 대답은 다소 황당할 수 있겠지만 당연한 내용이다. 무언가 테스트를 할 때는 아래와 같은 의식의 흐름으로 진행한다.",
16 | "link": "https://ui.toast.com/weekly-pick/ko_20201126"
17 | },
18 | {
19 | "title": "Async Cookie Store API 살펴보기",
20 | "summary": "document.cookie를 통해 쿠키를 가져오는 이상한 방법에 지쳤는가? 쿠키가 실제로 저장되었는지 여부를 알 수 없는 것이 싫었는가? 그렇다면, 크롬 87버전부터 사용 가능한 새로운 Cookie Store API에 대해 살펴볼 필요가 있다.",
21 | "link": "https://ui.toast.com/weekly-pick/ko_20201027"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/chapter11-tools/sass/data/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "CSS 중앙 정렬하기",
5 | "summary": "여러가지 테스트를 통해 5가지 중앙 정렬 방법 중 어떤 것이 가장 변화에 탄력적으로 대응하는지 확인해보자.",
6 | "link": "https://ui.toast.com/weekly-pick/ko_20201222"
7 | },
8 | {
9 | "title": "LCP(Largest Contentful Paint) 최적화하기",
10 | "summary": "Largest Contentful Paint(LCP)는 Core Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정한다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.",
11 | "link": "https://ui.toast.com/weekly-pick/ko_202012101720"
12 | },
13 | {
14 | "title": "useEffect를 테스트 하는 방법",
15 | "summary": "React.useEffect 코드를 사용하다 보면 도대체 이것들을 어떻게 테스트해야 할까 하는 궁금증이 생길 수 있다. 그리고 꽤 자주 묻곤 하는 질문이기도 하다. 이에 대한 대답은 다소 황당할 수 있겠지만 당연한 내용이다. 무언가 테스트를 할 때는 아래와 같은 의식의 흐름으로 진행한다.",
16 | "link": "https://ui.toast.com/weekly-pick/ko_20201126"
17 | },
18 | {
19 | "title": "Async Cookie Store API 살펴보기",
20 | "summary": "document.cookie를 통해 쿠키를 가져오는 이상한 방법에 지쳤는가? 쿠키가 실제로 저장되었는지 여부를 알 수 없는 것이 싫었는가? 그렇다면, 크롬 87버전부터 사용 가능한 새로운 Cookie Store API에 대해 살펴볼 필요가 있다.",
21 | "link": "https://ui.toast.com/weekly-pick/ko_20201027"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/chapter11-tools/bundler/data/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "CSS 중앙 정렬하기",
5 | "summary": "여러가지 테스트를 통해 5가지 중앙 정렬 방법 중 어떤 것이 가장 변화에 탄력적으로 대응하는지 확인해보자.",
6 | "link": "https://ui.toast.com/weekly-pick/ko_20201222"
7 | },
8 | {
9 | "title": "LCP(Largest Contentful Paint) 최적화하기",
10 | "summary": "Largest Contentful Paint(LCP)는 Core Web Vitals의 지표이며 뷰포트에서 가장 큰 콘텐츠 엘리먼트가 나타날 때 측정한다. 페이지의 주요 내용이 화면에 렌더링이 완료되는 시기를 결정하는데 사용된다.",
11 | "link": "https://ui.toast.com/weekly-pick/ko_202012101720"
12 | },
13 | {
14 | "title": "useEffect를 테스트 하는 방법",
15 | "summary": "React.useEffect 코드를 사용하다 보면 도대체 이것들을 어떻게 테스트해야 할까 하는 궁금증이 생길 수 있다. 그리고 꽤 자주 묻곤 하는 질문이기도 하다. 이에 대한 대답은 다소 황당할 수 있겠지만 당연한 내용이다. 무언가 테스트를 할 때는 아래와 같은 의식의 흐름으로 진행한다.",
16 | "link": "https://ui.toast.com/weekly-pick/ko_20201126"
17 | },
18 | {
19 | "title": "Async Cookie Store API 살펴보기",
20 | "summary": "document.cookie를 통해 쿠키를 가져오는 이상한 방법에 지쳤는가? 쿠키가 실제로 저장되었는지 여부를 알 수 없는 것이 싫었는가? 그렇다면, 크롬 87버전부터 사용 가능한 새로운 Cookie Store API에 대해 살펴볼 필요가 있다.",
21 | "link": "https://ui.toast.com/weekly-pick/ko_20201027"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/chapter13-test/test/unit/memoInputArea.spec.js:
--------------------------------------------------------------------------------
1 | import { screen } from "@testing-library/dom";
2 | import userEvent from "@testing-library/user-event";
3 | import MemoInputArea from "../../src/memoInputArea";
4 |
5 | describe("memoInputArea", () => {
6 | let memoInputArea, el, spy;
7 |
8 | beforeEach(() => {
9 | spy = jest.fn();
10 | el = document.createElement("div");
11 | memoInputArea = new MemoInputArea(el, { addMemoItem: spy });
12 |
13 | document.body.append(el);
14 | });
15 |
16 | afterEach(() => {
17 | el.remove();
18 | });
19 |
20 | it("MemoInputArea 요소가 렌더링된다.", () => {
21 | expect(el).toContainElement(memoInputArea.inputAreaEl);
22 | });
23 |
24 | it("MemoInputArea 요소는 스냅샷과 일치해야 한다.", () => {
25 | expect(memoInputArea.inputAreaEl).toMatchSnapshot();
26 | });
27 |
28 | it("Add Memo 버튼을 누를 시 입력된 텍스트 값을 인자로 받는 addMemoItem() 함수가 호출된다.", () => {
29 | const button = screen.getByText("Add Memo");
30 | const textarea = screen.getByLabelText("메모 작성하기");
31 |
32 | userEvent.type(textarea, "Bob");
33 | userEvent.click(button);
34 |
35 | expect(spy).toHaveBeenCalledWith("Bob");
36 | expect(textarea).toHaveValue("");
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/chapter13-test/src/memoItem.js:
--------------------------------------------------------------------------------
1 | import Button from "./button";
2 | import { addClass } from "./util/dom";
3 |
4 | let id = 0;
5 |
6 | export default class MemoItem {
7 | constructor(el, props) {
8 | this.el = el;
9 | this.props = props;
10 | this.id = id++;
11 | this.render();
12 | }
13 |
14 | render() {
15 | const itemEl = document.createElement("li");
16 |
17 | addClass(
18 | itemEl,
19 | "list-group-item",
20 | "d-flex",
21 | "justify-content-between",
22 | "align-items-center"
23 | );
24 |
25 | this.itemEl = itemEl;
26 | this.renderContent();
27 | this.renderDeleteMemoBtn();
28 |
29 | this.el.append(itemEl);
30 | }
31 |
32 | renderContent() {
33 | const span = document.createElement("span");
34 | addClass(span, "text-break");
35 |
36 | span.textContent = this.props.content;
37 |
38 | this.itemEl.append(span);
39 | }
40 |
41 | renderDeleteMemoBtn() {
42 | this.button = new Button(this.itemEl, {
43 | attributes: { "data-item-id": this.id },
44 | classNames: ["btn", "btn-danger", "btn-sm"],
45 | textContent: "X",
46 | });
47 | }
48 |
49 | destroy() {
50 | this.itemEl.remove();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/chapter12-debug/Readme.md:
--------------------------------------------------------------------------------
1 | # 12장. 디버깅
2 |
3 | ## 설치
4 |
5 | ```
6 | npm i
7 | ```
8 |
9 | ## 개발서버 실행
10 |
11 | ```
12 | npm run start
13 | ```
14 |
15 | ## 커밋별 진행
16 |
17 | 1. 프로젝트 초기 상태
18 | * 링크: https://github.com/bjpublic/Front-end/commit/0653960a05bd022af8a3e03432430d71f21bd8fe
19 | 2. 12.2.1 문제 수정(모바일 환경 미디어 쿼리 추가)
20 | * 링크: https://github.com/bjpublic/Front-end/commit/f622ea5f49e4cfc5ddcd6f03b4fc94f09865e385
21 | 3. 12.2.2 문제 수정(css 위치 수정, 클래스 명 변경)
22 | * 링크: https://github.com/bjpublic/Front-end/commit/6bd21a48e2318e799ca0852fcf031feaac50290d
23 | 4. 12.2.3 경고 문구 위치 클릭시 에러가 발생하는 상황
24 | * 링크: https://github.com/bjpublic/Front-end/commit/dbe12a044fe40f6d03fd3007b88c569c365df3b2
25 | 5. 12.2.3 문제 수정(이벤트 바인딩 방식 수정)
26 | * 링크: https://github.com/bjpublic/Front-end/commit/b94678bca6bccf31ab0ac34b5a33cd83051092fa
27 | 6. 12.2.4 이벤트 리스너로 인해 제대로 렌더링 되지 않는 상황
28 | * 링크: https://github.com/bjpublic/Front-end/commit/94a527488363c9bf89ff4c5a1c3de0518fb3a437
29 | 7. 12.2.4 문제 수정(이벤트 리스너 오타 수정)
30 | * 링크: https://github.com/bjpublic/Front-end/commit/e05ebcba36000750f5fbbe545132267fd0d01a5c
31 | 8. 12.2.5 문제 수정(인덱스 isNaN 확인 후 사용)
32 | * 링크: https://github.com/bjpublic/Front-end/commit/b66ddaf23305667de61e7caf2b202c84cf7e6b28
33 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter11-tools",
3 | "version": "1.0.0",
4 | "description": "development tools example - npm, webpack, babel, scss, lint",
5 | "scripts": {
6 | "my-script": "node ./myScript.js",
7 | "serve": "webpack serve",
8 | "build:dev": "webpack --config webpack.config.js --mode=development",
9 | "build:prod": "webpack --config webpack.config.js --mode=production",
10 | "transpile": "babel src/js -d transpile"
11 | },
12 | "author": "jaesung lee, jung han",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "@babel/cli": "^7.12.0",
16 | "@babel/core": "^7.12.9",
17 | "@babel/preset-env": "^7.12.7",
18 | "babel-loader": "^8.2.2",
19 | "clean-webpack-plugin": "^3.0.0",
20 | "css-loader": "^5.0.1",
21 | "css-minimizer-webpack-plugin": "^1.1.5",
22 | "file-loader": "^6.2.0",
23 | "html-webpack-plugin": "^4.5.0",
24 | "mini-css-extract-plugin": "^1.3.3",
25 | "optimize-css-assets-webpack-plugin": "^5.0.4",
26 | "style-loader": "^2.0.0",
27 | "webpack": "^5.10.0",
28 | "webpack-cli": "^4.3.0",
29 | "webpack-dev-server": "^3.11.0"
30 | },
31 | "dependencies": {
32 | "core-js": "^3.8.1",
33 | "whatwg-fetch": "^3.5.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
7 |
8 | module.exports = {
9 | mode: 'production',
10 | entry: './src/js/main.js',
11 | output: {
12 | path: path.resolve(process.cwd(), 'dist'),
13 | filename: '[name].[contenthash].js',
14 | environment: {
15 | arrowFunction: false,
16 | },
17 | },
18 | plugins: [
19 | new HtmlWebpackPlugin({ template: 'index.html' }),
20 | new MiniCssExtractPlugin(),
21 | new CleanWebpackPlugin(),
22 | ],
23 | module: {
24 | rules: [
25 | {
26 | test: /\.css$/,
27 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
28 | },
29 | {
30 | test: /\.(png|jpg|gif)$/i,
31 | use: ['file-loader'],
32 | }
33 | ],
34 | },
35 | optimization: {
36 | minimize: true,
37 | minimizer: [
38 | new TerserPlugin({
39 | extractComments: false,
40 | }),
41 | new CssMinimizerPlugin(),
42 | ],
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/chapter12-debug/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | /* 12.2.2 클래스명 변경 */
8 | .card-container {
9 | padding: 30px;
10 | }
11 |
12 | .h1 {
13 | text-align: center;
14 | }
15 |
16 | .card {
17 | cursor: pointer;
18 | margin-bottom: 10px;
19 | }
20 |
21 | .card:hover {
22 | background: rgba(0, 0, 0, 0.1);
23 | }
24 |
25 | .modal-layer {
26 | background: rgba(0, 0, 0, 0.8);
27 | width: 100vw;
28 | height: 100vh;
29 | position: fixed;
30 | top: 0;
31 | display: flex;
32 | flex-direction: column;
33 | justify-content: center;
34 | align-items: center;
35 | }
36 |
37 | .modal-window {
38 | background-color: white;
39 | width: 80%;
40 | padding: 5%;
41 | border-radius: 10px;
42 | display: flex;
43 | flex-direction: column;
44 | }
45 |
46 | .modal-layer.hidden {
47 | visibility: hidden;
48 | }
49 |
50 | .modal-layer.visible {
51 | visibility: visible;
52 | }
53 |
54 | .card-area {
55 | display: flex;
56 | flex-flow: wrap;
57 | justify-content: space-between;
58 | }
59 |
60 | .notice {
61 | width: 100%;
62 | text-align: center;
63 | }
64 |
65 | /* 12.2.1 모바일 환경 적용 */
66 | @media (min-width: 768px) { .card-area .card {
67 | width: 32%; }
68 | }
69 |
70 | @media (max-width: 768px) { .card-area .card {
71 | width: 47%; }
72 | }
--------------------------------------------------------------------------------
/chapter10-board/data/top.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "JavaScript의 25번째 생일을 축하합니다!",
5 | "summary": "1996년 Netscape와 Sun Microsystems가 발표한 JavaScript가 2020년, 25주년을 맞이하게 되었습니다!",
6 | "link": "https://js25.org",
7 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101241806-c786d580-373c-11eb-86ba-ed405cfc2ed2.png"
8 | },
9 | {
10 | "title": "Webpack 5.0 릴리즈",
11 | "summary": "2020년 10월 10일 새로운 웹팩 버전인 Webpack 5.0이 릴리즈 되었습니다. 이번 메이저 배포에서 어떤 변화가 있었는지 함께 살펴보시죠.",
12 | "link": "https://webpack.js.org/blog/2020-10-10-webpack-5-release/",
13 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101242849-77f5d900-373f-11eb-8070-7cb865009c1f.png"
14 | },
15 | {
16 | "title": "JSConf 개최",
17 | "summary": "올 연말 가장 큰 규모의 자바스크립트 컨퍼런스인 JSConf가 웨비나로 개최됩니다.",
18 | "link": "https://jsconf.com/",
19 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243080-1df61300-3741-11eb-879a-1f25bb38d3ff.jpg"
20 | },
21 | {
22 | "title": "[개발자 인터뷰] NHN 개발자 이재성",
23 | "summary": "오늘의 개발자 인터뷰는 NHN에서 자바스크립트로 개발을 하고 있으신 이재성씨를 만나봤습니다.",
24 | "link": "#1",
25 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243159-9eb50f00-3741-11eb-8e84-33a394179621.jpg"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/data/top.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "JavaScript의 25번째 생일을 축하합니다!",
5 | "summary": "1996년 Netscape와 Sun Microsystems가 발표한 JavaScript가 2020년, 25주년을 맞이하게 되었습니다!",
6 | "link": "https://js25.org",
7 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101241806-c786d580-373c-11eb-86ba-ed405cfc2ed2.png"
8 | },
9 | {
10 | "title": "Webpack 5.0 릴리즈",
11 | "summary": "2020년 10월 10일 새로운 웹팩 버전인 Webpack 5.0이 릴리즈 되었습니다. 이번 메이저 배포에서 어떤 변화가 있었는지 함께 살펴보시죠.",
12 | "link": "https://webpack.js.org/blog/2020-10-10-webpack-5-release/",
13 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101242849-77f5d900-373f-11eb-8070-7cb865009c1f.png"
14 | },
15 | {
16 | "title": "JSConf 개최",
17 | "summary": "올 연말 가장 큰 규모의 자바스크립트 컨퍼런스인 JSConf가 웨비나로 개최됩니다.",
18 | "link": "https://jsconf.com/",
19 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243080-1df61300-3741-11eb-879a-1f25bb38d3ff.jpg"
20 | },
21 | {
22 | "title": "[개발자 인터뷰] NHN 개발자 이재성",
23 | "summary": "오늘의 개발자 인터뷰는 NHN에서 자바스크립트로 개발을 하고 있으신 이재성씨를 만나봤습니다.",
24 | "link": "#1",
25 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243159-9eb50f00-3741-11eb-8e84-33a394179621.jpg"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/chapter11-tools/lint/data/top.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "JavaScript의 25번째 생일을 축하합니다!",
5 | "summary": "1996년 Netscape와 Sun Microsystems가 발표한 JavaScript가 2020년, 25주년을 맞이하게 되었습니다!",
6 | "link": "https://js25.org",
7 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101241806-c786d580-373c-11eb-86ba-ed405cfc2ed2.png"
8 | },
9 | {
10 | "title": "Webpack 5.0 릴리즈",
11 | "summary": "2020년 10월 10일 새로운 웹팩 버전인 Webpack 5.0이 릴리즈 되었습니다. 이번 메이저 배포에서 어떤 변화가 있었는지 함께 살펴보시죠.",
12 | "link": "https://webpack.js.org/blog/2020-10-10-webpack-5-release/",
13 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101242849-77f5d900-373f-11eb-8070-7cb865009c1f.png"
14 | },
15 | {
16 | "title": "JSConf 개최",
17 | "summary": "올 연말 가장 큰 규모의 자바스크립트 컨퍼런스인 JSConf가 웨비나로 개최됩니다.",
18 | "link": "https://jsconf.com/",
19 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243080-1df61300-3741-11eb-879a-1f25bb38d3ff.jpg"
20 | },
21 | {
22 | "title": "[개발자 인터뷰] NHN 개발자 이재성",
23 | "summary": "오늘의 개발자 인터뷰는 NHN에서 자바스크립트로 개발을 하고 있으신 이재성씨를 만나봤습니다.",
24 | "link": "#1",
25 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243159-9eb50f00-3741-11eb-8e84-33a394179621.jpg"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/chapter11-tools/npm/data/top.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "JavaScript의 25번째 생일을 축하합니다!",
5 | "summary": "1996년 Netscape와 Sun Microsystems가 발표한 JavaScript가 2020년, 25주년을 맞이하게 되었습니다!",
6 | "link": "https://js25.org",
7 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101241806-c786d580-373c-11eb-86ba-ed405cfc2ed2.png"
8 | },
9 | {
10 | "title": "Webpack 5.0 릴리즈",
11 | "summary": "2020년 10월 10일 새로운 웹팩 버전인 Webpack 5.0이 릴리즈 되었습니다. 이번 메이저 배포에서 어떤 변화가 있었는지 함께 살펴보시죠.",
12 | "link": "https://webpack.js.org/blog/2020-10-10-webpack-5-release/",
13 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101242849-77f5d900-373f-11eb-8070-7cb865009c1f.png"
14 | },
15 | {
16 | "title": "JSConf 개최",
17 | "summary": "올 연말 가장 큰 규모의 자바스크립트 컨퍼런스인 JSConf가 웨비나로 개최됩니다.",
18 | "link": "https://jsconf.com/",
19 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243080-1df61300-3741-11eb-879a-1f25bb38d3ff.jpg"
20 | },
21 | {
22 | "title": "[개발자 인터뷰] NHN 개발자 이재성",
23 | "summary": "오늘의 개발자 인터뷰는 NHN에서 자바스크립트로 개발을 하고 있으신 이재성씨를 만나봤습니다.",
24 | "link": "#1",
25 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243159-9eb50f00-3741-11eb-8e84-33a394179621.jpg"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/chapter11-tools/sass/data/top.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "JavaScript의 25번째 생일을 축하합니다!",
5 | "summary": "1996년 Netscape와 Sun Microsystems가 발표한 JavaScript가 2020년, 25주년을 맞이하게 되었습니다!",
6 | "link": "https://js25.org",
7 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101241806-c786d580-373c-11eb-86ba-ed405cfc2ed2.png"
8 | },
9 | {
10 | "title": "Webpack 5.0 릴리즈",
11 | "summary": "2020년 10월 10일 새로운 웹팩 버전인 Webpack 5.0이 릴리즈 되었습니다. 이번 메이저 배포에서 어떤 변화가 있었는지 함께 살펴보시죠.",
12 | "link": "https://webpack.js.org/blog/2020-10-10-webpack-5-release/",
13 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101242849-77f5d900-373f-11eb-8070-7cb865009c1f.png"
14 | },
15 | {
16 | "title": "JSConf 개최",
17 | "summary": "올 연말 가장 큰 규모의 자바스크립트 컨퍼런스인 JSConf가 웨비나로 개최됩니다.",
18 | "link": "https://jsconf.com/",
19 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243080-1df61300-3741-11eb-879a-1f25bb38d3ff.jpg"
20 | },
21 | {
22 | "title": "[개발자 인터뷰] NHN 개발자 이재성",
23 | "summary": "오늘의 개발자 인터뷰는 NHN에서 자바스크립트로 개발을 하고 있으신 이재성씨를 만나봤습니다.",
24 | "link": "#1",
25 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243159-9eb50f00-3741-11eb-8e84-33a394179621.jpg"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/chapter11-tools/bundler/data/top.json:
--------------------------------------------------------------------------------
1 | {
2 | "articles": [
3 | {
4 | "title": "JavaScript의 25번째 생일을 축하합니다!",
5 | "summary": "1996년 Netscape와 Sun Microsystems가 발표한 JavaScript가 2020년, 25주년을 맞이하게 되었습니다!",
6 | "link": "https://js25.org",
7 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101241806-c786d580-373c-11eb-86ba-ed405cfc2ed2.png"
8 | },
9 | {
10 | "title": "Webpack 5.0 릴리즈",
11 | "summary": "2020년 10월 10일 새로운 웹팩 버전인 Webpack 5.0이 릴리즈 되었습니다. 이번 메이저 배포에서 어떤 변화가 있었는지 함께 살펴보시죠.",
12 | "link": "https://webpack.js.org/blog/2020-10-10-webpack-5-release/",
13 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101242849-77f5d900-373f-11eb-8070-7cb865009c1f.png"
14 | },
15 | {
16 | "title": "JSConf 개최",
17 | "summary": "올 연말 가장 큰 규모의 자바스크립트 컨퍼런스인 JSConf가 웨비나로 개최됩니다.",
18 | "link": "https://jsconf.com/",
19 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243080-1df61300-3741-11eb-879a-1f25bb38d3ff.jpg"
20 | },
21 | {
22 | "title": "[개발자 인터뷰] NHN 개발자 이재성",
23 | "summary": "오늘의 개발자 인터뷰는 NHN에서 자바스크립트로 개발을 하고 있으신 이재성씨를 만나봤습니다.",
24 | "link": "#1",
25 | "thumbnailImage": "https://user-images.githubusercontent.com/35371660/101243159-9eb50f00-3741-11eb-8e84-33a394179621.jpg"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/chapter11-tools/sass/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter11-tools",
3 | "version": "1.0.0",
4 | "description": "development tools example - npm, webpack, babel, scss, lint",
5 | "scripts": {
6 | "my-script": "node ./myScript.js",
7 | "serve": "webpack serve",
8 | "build:dev": "webpack --config webpack.config.js --mode=development",
9 | "build:prod": "webpack --config webpack.config.js --mode=production",
10 | "transpile": "babel src/js -d transpile",
11 | "transpile:sass": "sass scss/test.scss:scss/test.css"
12 | },
13 | "author": "jaesung lee, jung han",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "@babel/cli": "^7.12.0",
17 | "@babel/core": "^7.12.9",
18 | "@babel/preset-env": "^7.12.7",
19 | "babel-loader": "^8.2.2",
20 | "clean-webpack-plugin": "^3.0.0",
21 | "css-loader": "^5.0.1",
22 | "css-minimizer-webpack-plugin": "^1.1.5",
23 | "file-loader": "^6.2.0",
24 | "html-webpack-plugin": "^4.5.0",
25 | "mini-css-extract-plugin": "^1.3.3",
26 | "optimize-css-assets-webpack-plugin": "^5.0.4",
27 | "sass": "^1.30.0",
28 | "sass-loader": "^10.1.0",
29 | "style-loader": "^2.0.0",
30 | "webpack": "^5.10.0",
31 | "webpack-cli": "^4.3.0",
32 | "webpack-dev-server": "^3.11.0"
33 | },
34 | "dependencies": {
35 | "core-js": "^3.8.1",
36 | "whatwg-fetch": "^3.5.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/chapter13-test/test/integration/memoList.spec.js:
--------------------------------------------------------------------------------
1 | import { screen } from "@testing-library/dom";
2 | import MemoList from "../../src/memoList";
3 |
4 | describe("MemoList", () => {
5 | let memoList, el;
6 |
7 | beforeEach(() => {
8 | el = document.createElement("div");
9 |
10 | memoList = new MemoList(el);
11 |
12 | document.body.append(el);
13 | });
14 |
15 | afterEach(() => {
16 | el.remove();
17 | });
18 |
19 | it("memoList 요소가 렌더링된다.", () => {
20 | expect(document.body).toContainElement(memoList.list);
21 | });
22 |
23 | it("memoList 요소는 스냅샷과 일치해야 한다.", () => {
24 | expect(memoList.list).toMatchSnapshot();
25 | });
26 |
27 | it("addMemoItem() 메서드 호출 시 메모 항목이 순서대로 추가된다.", () => {
28 | memoList.addMemoItem("memo1");
29 | memoList.addMemoItem("memo2");
30 |
31 | const firstMemo = screen.getByText("memo1");
32 | const secondMemo = screen.getByText("memo2");
33 |
34 | expect(memoList.list.childNodes[0]).toContainElement(firstMemo);
35 | expect(memoList.list.childNodes[1]).toContainElement(secondMemo);
36 | });
37 |
38 | it("메모 리스트의 삭제 버튼 클릭 시 해당 메모 항목이 삭제된다.", () => {
39 | memoList.addMemoItem("memo1");
40 |
41 | const memo = screen.getByText("memo1");
42 |
43 | const deleteBtn = screen.getByText("X");
44 | deleteBtn.click();
45 |
46 | expect(memoList.list).not.toContainElement(memo);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
7 |
8 | module.exports = {
9 | mode: 'production',
10 | entry: './src/js/main.js',
11 | output: {
12 | path: path.resolve(process.cwd(), 'dist'),
13 | filename: '[name].[contenthash].js',
14 | environment: {
15 | arrowFunction: false,
16 | },
17 | },
18 | plugins: [
19 | new HtmlWebpackPlugin({ template: 'index.html' }),
20 | new MiniCssExtractPlugin(),
21 | new CleanWebpackPlugin(),
22 | ],
23 | module: {
24 | rules: [
25 | {
26 | test: /\.css$/,
27 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
28 | },
29 | {
30 | test: /\.(png|jpg|gif)$/i,
31 | use: ['file-loader'],
32 | },
33 | {
34 | test: /\.js$/,
35 | include: [path.resolve(__dirname, 'src/js')],
36 | exclude: /node_modules/,
37 | use: { loader: 'babel-loader' },
38 | }
39 | ],
40 | },
41 | optimization: {
42 | minimize: true,
43 | minimizer: [
44 | new TerserPlugin({
45 | extractComments: false,
46 | }),
47 | new CssMinimizerPlugin(),
48 | ],
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/chapter13-test/Readme.md:
--------------------------------------------------------------------------------
1 | # 13장. 프론트엔드 테스트
2 |
3 | ## 설치
4 |
5 | ```
6 | npm ci
7 | // or
8 | npm install
9 | ```
10 |
11 | ## 의존성 설치
12 |
13 | ```sh
14 | npm ci
15 | // or
16 | npm install
17 | ```
18 |
19 | ## jest 단위, 통합 테스트 실행
20 |
21 | ```sh
22 | npm test
23 | ```
24 |
25 | ## cypress e2e 테스트 실행
26 |
27 | ```sh
28 | npm run e2e-test
29 | ```
30 |
31 | ## percy visual 테스트 실행
32 |
33 | 1. https://percy.io/에서 회원 가입 후 로그인을 합니다.
34 | 2. 로그인 시 앱 화면에 진입 하게 되는데 이 때 create new project를 클릭해 프로젝트를 생성합니다.
35 | 
36 |
37 | 3. 프로젝트가 생성되면 발급되는 토큰을 각 운영체제에 맞게 프로젝트 디렉토리의 환경 변수로 등록합니다. (ex.`.env` 파일)
38 | 
39 |
40 | ```sh
41 | npm run percy
42 | ```
43 |
44 | - env 파일 설정은 아래 링크를 참고해주세요.
45 | - dot-env: https://www.npmjs.com/package/dot-env
46 | - cross-env: https://www.npmjs.com/package/cross-env
47 |
48 | ## 커밋별 진행
49 |
50 | 1. 프로젝트 세팅, 메모 어플리케이션 추가
51 |
52 | - 링크: https://github.com/bjpublic/Front-end/commit/eb8089cb1b131dba6334f55cd8ad95eff8e0a29e
53 |
54 | 2. jest 단위, 통합, 스냅샷 테스트 추가
55 |
56 | - 링크: https://github.com/bjpublic/Front-end/commit/8992a384cb452fcb17b72cc90f3baf58bca8fbdf
57 |
58 | 3. cypress e2e 테스트 추가
59 |
60 | - 링크: https://github.com/bjpublic/Front-end/commit/389368f54ed8daf1fd4f0b43cf7deec16d6babc6
61 |
62 | 4. Storybook 추가
63 |
64 | - 링크: https://github.com/bjpublic/Front-end/commit/3513aec5c77742b435a5163d95c95c037db06ac3
65 |
--------------------------------------------------------------------------------
/chapter13-test/test/unit/button.spec.js:
--------------------------------------------------------------------------------
1 | import userEvent from "@testing-library/user-event";
2 | import Button from "../../src/button";
3 |
4 | describe("button 컴포넌트", () => {
5 | let el;
6 |
7 | beforeEach(() => {
8 | el = document.createElement("div");
9 | });
10 |
11 | it("button 요소가 렌더링된다.", () => {
12 | const button = new Button(el);
13 |
14 | expect(el).toContainElement(button.buttonEl);
15 | });
16 |
17 | it("button 요소는 스냅샷과 일치해야 한다.", () => {
18 | const button = new Button(el);
19 |
20 | expect(button.buttonEl).toMatchSnapshot();
21 | });
22 |
23 | it("classNames 옵션이 적용되어야 한다.", () => {
24 | const button = new Button(el, { classNames: ["my-class1", "my-class2"] });
25 |
26 | expect(button.buttonEl).toHaveClass("my-class1");
27 | expect(button.buttonEl).toHaveClass("my-class2");
28 | });
29 |
30 | it("attributes 옵션이 적용되어야 한다.", () => {
31 | const button = new Button(el, {
32 | attributes: { id: 1, "data-test-id": "test-id" },
33 | });
34 |
35 | expect(button.buttonEl).toHaveAttribute("id", "1");
36 | expect(button.buttonEl).toHaveAttribute("data-test-id", "test-id");
37 | });
38 |
39 | it("textContent 옵션이 적용되어야 한다.", () => {
40 | const button = new Button(el, { textContent: "test" });
41 |
42 | expect(button.buttonEl).toHaveTextContent("test");
43 | });
44 |
45 | it("이벤트 리스너가 등록되어야 한다.", () => {
46 | const spy = jest.fn();
47 | const button = new Button(el, { onClick: spy });
48 |
49 | userEvent.click(button.buttonEl);
50 |
51 | expect(spy).toHaveBeenCalledTimes(1);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/chapter12-debug/js/frameworkCard.js:
--------------------------------------------------------------------------------
1 | import { showModal } from "./modal";
2 |
3 | export default class FrameworkCard {
4 | constructor(frameworkInfo) {
5 | const { name, desc, website, imageUrl, index } = frameworkInfo;
6 |
7 | this.name = name;
8 | this.formattedName = this.formatName(name, index);
9 | this.desc = desc;
10 | this.website = website;
11 | this.imageUrl = imageUrl;
12 |
13 | this.el = this.makeCardElement();
14 | }
15 |
16 | formatName(name, index) {
17 | const iconList = ["🦊", "🐶", "🐱"];
18 | const icon = Number.isNaN(Number(index)) ? '🦁' : iconList[index];
19 |
20 | return `${icon} ${name}`;
21 | }
22 |
23 | getName() {
24 | return this.name;
25 | }
26 |
27 | getDesc() {
28 | return this.desc;
29 | }
30 |
31 | getWebsite() {
32 | return this.website;
33 | }
34 |
35 | getImageUrl() {
36 | return this.imageUrl;
37 | }
38 |
39 | onClickCard() {
40 | const { name, desc, website } = this;
41 | showModal({ name, desc, website });
42 | }
43 |
44 | makeCardElement() {
45 | const el = document.createElement("div");
46 | el.className = "card";
47 | el.id = this.name;
48 | el.innerHTML = `
49 |
50 |
51 |
${this.formattedName}
52 |
53 | ${this.desc}
54 |
55 |
56 | `;
57 |
58 | el.addEventListener("click", this.onClickCard.bind(this));
59 |
60 | return el;
61 | }
62 | }
--------------------------------------------------------------------------------
/chapter11-tools/lint/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
7 |
8 | module.exports = {
9 | mode: 'production',
10 | entry: './src/js/main.js',
11 | output: {
12 | path: path.resolve(process.cwd(), 'dist'),
13 | filename: '[name].[contenthash].js',
14 | environment: {
15 | arrowFunction: false,
16 | },
17 | },
18 | plugins: [
19 | new HtmlWebpackPlugin({ template: 'index.html' }),
20 | new MiniCssExtractPlugin(),
21 | new CleanWebpackPlugin(),
22 | ],
23 | module: {
24 | rules: [
25 | {
26 | test: /\.css$/,
27 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
28 | },
29 | {
30 | test: /\.(png|jpg|gif)$/i,
31 | use: ['file-loader'],
32 | },
33 | {
34 | test: /\.js$/,
35 | include: [path.resolve(__dirname, 'src/js')],
36 | exclude: /node_modules/,
37 | use: { loader: 'babel-loader' },
38 | },
39 | {
40 | test: /\.scss$/i,
41 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
42 | },
43 | ],
44 | },
45 | optimization: {
46 | minimize: true,
47 | minimizer: [
48 | new TerserPlugin({
49 | extractComments: false,
50 | }),
51 | new CssMinimizerPlugin(),
52 | ],
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
7 |
8 | module.exports = {
9 | mode: 'production',
10 | entry: './src/js/main.js',
11 | output: {
12 | path: path.resolve(process.cwd(), 'dist'),
13 | filename: '[name].[contenthash].js',
14 | environment: {
15 | arrowFunction: false,
16 | },
17 | },
18 | plugins: [
19 | new HtmlWebpackPlugin({ template: 'index.html' }),
20 | new MiniCssExtractPlugin(),
21 | new CleanWebpackPlugin(),
22 | ],
23 | module: {
24 | rules: [
25 | {
26 | test: /\.css$/,
27 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
28 | },
29 | {
30 | test: /\.(png|jpg|gif)$/i,
31 | use: ['file-loader'],
32 | },
33 | {
34 | test: /\.js$/,
35 | include: [path.resolve(__dirname, 'src/js')],
36 | exclude: /node_modules/,
37 | use: { loader: 'babel-loader' },
38 | },
39 | {
40 | test: /\.scss$/i,
41 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
42 | },
43 | ],
44 | },
45 | optimization: {
46 | minimize: true,
47 | minimizer: [
48 | new TerserPlugin({
49 | extractComments: false,
50 | }),
51 | new CssMinimizerPlugin(),
52 | ],
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/chapter13-test/src/memoInputArea.js:
--------------------------------------------------------------------------------
1 | import { addClass } from "./util/dom";
2 | import Button from "./button";
3 |
4 | export default class MemoInputArea {
5 | constructor(el, props) {
6 | this.el = el;
7 | this.props = props;
8 | this.render();
9 | }
10 |
11 | render() {
12 | const inputAreaEl = document.createElement("div");
13 |
14 | addClass(inputAreaEl, "border-bottom", "mb-3", "pb-3");
15 |
16 | this.inputAreaEl = inputAreaEl;
17 | this.renderTextarea();
18 | this.renderAddMemoBtn();
19 |
20 | this.el.append(inputAreaEl);
21 | }
22 |
23 | renderAddMemoBtn() {
24 | const wrapper = document.createElement("div");
25 |
26 | this.button = new Button(wrapper, {
27 | classNames: ["btn", "btn-primary"],
28 | textContent: "Add Memo",
29 | onClick: this.addMemoItem,
30 | });
31 |
32 | addClass(wrapper, "d-flex", "justify-content-end", "mt-3");
33 |
34 | this.inputAreaEl.append(wrapper);
35 | }
36 |
37 | renderTextarea() {
38 | const wrapper = document.createElement("div");
39 | const textArea = document.createElement("textarea");
40 | const label = document.createElement("label");
41 |
42 | addClass(wrapper, "form-floating");
43 | addClass(textArea, "form-control");
44 |
45 | textArea.style.height = "100px";
46 | textArea.id = "memo";
47 |
48 | label.setAttribute("for", "memo");
49 | label.textContent = "메모 작성하기";
50 |
51 | this.textArea = textArea;
52 |
53 | wrapper.append(textArea);
54 | wrapper.append(label);
55 |
56 | this.inputAreaEl.append(wrapper);
57 | }
58 |
59 | addMemoItem = () => {
60 | this.props.addMemoItem(this.textArea.value);
61 | this.textArea.value = "";
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter11-tools",
3 | "version": "1.0.0",
4 | "description": "development tools example - npm, webpack, babel, scss",
5 | "scripts": {
6 | "my-script": "node ./myScript.js",
7 | "serve": "webpack serve",
8 | "build:dev": "webpack --config webpack.config.js --mode=development",
9 | "build:prod": "webpack --config webpack.config.js --mode=production",
10 | "transpile": "babel src/js -d transpile",
11 | "transpile:scss": "sass scss/test.scss:scss/test.css",
12 | "prettify": "prettier --write ."
13 | },
14 | "author": "jaesung lee, jung han",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "@babel/cli": "^7.12.0",
18 | "@babel/core": "^7.12.9",
19 | "@babel/preset-env": "^7.12.7",
20 | "babel-loader": "^8.2.2",
21 | "clean-webpack-plugin": "^3.0.0",
22 | "css-loader": "^5.0.1",
23 | "css-minimizer-webpack-plugin": "^1.1.5",
24 | "eslint": "^7.15.0",
25 | "eslint-config-prettier": "^7.0.0",
26 | "eslint-plugin-prettier": "^3.2.0",
27 | "file-loader": "^6.2.0",
28 | "html-webpack-plugin": "^4.5.0",
29 | "mini-css-extract-plugin": "^1.3.3",
30 | "optimize-css-assets-webpack-plugin": "^5.0.4",
31 | "prettier": "^2.2.1",
32 | "sass": "^1.30.0",
33 | "sass-loader": "^10.1.0",
34 | "style-loader": "^2.0.0",
35 | "stylelint": "^13.8.0",
36 | "stylelint-config-prettier": "^8.0.2",
37 | "stylelint-config-standard": "^20.0.0",
38 | "stylelint-prettier": "^1.1.2",
39 | "stylelint-scss": "^3.18.0",
40 | "webpack": "^5.10.0",
41 | "webpack-cli": "^4.3.0",
42 | "webpack-dev-server": "^3.11.0"
43 | },
44 | "dependencies": {
45 | "core-js": "^3.8.1",
46 | "whatwg-fetch": "^3.5.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/chapter13-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter13-test",
3 | "version": "1.0.0",
4 | "description": "13, 14장 테스트 예제",
5 | "scripts": {
6 | "start": "webpack serve",
7 | "storybook": "start-storybook -p 6006",
8 | "build-storybook": "build-storybook",
9 | "cy:open": "cypress open",
10 | "cy:run": "cypress run",
11 | "e2e-test": "start-server-and-test start http://localhost:8000 cy:open",
12 | "e2e-test:ci": "start-server-and-test start http://localhost:8000 cy:run",
13 | "test": "jest --watch",
14 | "percy": "build-storybook && percy-storybook --widths=640,1280"
15 | },
16 | "devDependencies": {
17 | "@babel/cli": "^7.12.16",
18 | "@babel/core": "^7.12.13",
19 | "@babel/eslint-parser": "^7.13.10",
20 | "@babel/plugin-proposal-class-properties": "^7.12.13",
21 | "@babel/preset-env": "^7.12.16",
22 | "@cypress/webpack-preprocessor": "^5.5.0",
23 | "@percy/storybook": "^4.0.3",
24 | "@storybook/addon-actions": "^6.3.10",
25 | "@storybook/addon-essentials": "^6.3.10",
26 | "@storybook/addon-links": "^6.3.10",
27 | "@storybook/html": "^6.3.10",
28 | "@testing-library/cypress": "^7.0.4",
29 | "@testing-library/dom": "^7.30.0",
30 | "@testing-library/jest-dom": "^5.11.9",
31 | "@testing-library/user-event": "^12.8.3",
32 | "babel-jest": "^26.6.3",
33 | "babel-loader": "^8.2.2",
34 | "css-loader": "^5.0.1",
35 | "cypress": "^6.4.0",
36 | "eslint": "^7.21.0",
37 | "eslint-config-prettier": "^8.1.0",
38 | "eslint-plugin-prettier": "^3.3.1",
39 | "html-webpack-plugin": "^5.0.0-beta.6",
40 | "jest": "^26.6.3",
41 | "jest-serializer-html": "^7.0.0",
42 | "prettier": "^2.2.1",
43 | "start-server-and-test": "^1.12.0",
44 | "style-loader": "^2.0.0",
45 | "webpack": "^5.17.0",
46 | "webpack-cli": "^4.4.0",
47 | "webpack-dev-server": "^3.11.2"
48 | },
49 | "keywords": [],
50 | "author": "",
51 | "license": "ISC",
52 | "dependencies": {
53 | "bootstrap": "5.0.0-beta1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/chapter13-test/cypress/integration/memo.spec.js:
--------------------------------------------------------------------------------
1 | beforeEach(() => {
2 | cy.visit("/");
3 | });
4 |
5 | function fillContent(content) {
6 | cy.findByLabelText("메모 작성하기").type(content);
7 | }
8 |
9 | function clickAddMemoBtn() {
10 | cy.findByText("Add Memo").click();
11 | }
12 |
13 | function clickDeleteBtn(text) {
14 | cy.findByText(text).parent().findByText("X").click();
15 | }
16 |
17 | function assertAlertText(text) {
18 | cy.on("window:alert", (str) => {
19 | expect(str).to.eq(text);
20 | });
21 | }
22 |
23 | function assertEmptyMemoTextarea(content) {
24 | cy.findByLabelText("메모 작성하기").should("have.value", "");
25 | }
26 |
27 | function assertNotHaveMemoItem(text) {
28 | cy.findByText(text).should("have.not.exist");
29 | }
30 |
31 | function assertOrderedMemoList(textList) {
32 | const list = cy.get("li");
33 |
34 | cy.get("li").each((el, index) => {
35 | expect(el.text()).to.contain(textList[index]);
36 | });
37 | }
38 |
39 | describe("Memo App", () => {
40 | // 사용자가 텍스트를 입력하고, Add Memo 버튼을 눌러 메모를 추가한다.
41 | // 메모는 순서대로 추가되며, 추가된 후 텍스트는 초기화된다.
42 | // 사용자가 추가된 메모 항목의 X 버튼을 누르면 해당 메모는 삭제된다.
43 | it("일반 사용자", () => {
44 | fillContent("TODO-1");
45 | clickAddMemoBtn();
46 |
47 | fillContent("TODO-2");
48 | clickAddMemoBtn();
49 |
50 | assertOrderedMemoList(["TODO-1", "TODO-2"]);
51 | assertEmptyMemoTextarea();
52 |
53 | clickDeleteBtn("TODO-1");
54 |
55 | assertNotHaveMemoItem("TODO-1");
56 | });
57 |
58 | // 텍스트를 입력하지 않은 상태로 Add Memo 버튼을 클릭했을 때, 내용을 입력해달라는 경고 창이 뜬다.
59 | it("메모를 입력하지 않은 사용자", () => {
60 | clickAddMemoBtn();
61 |
62 | assertAlertText("글을 작성해주세요!");
63 | });
64 |
65 | // 50자 이상의 글자를 입력한 뒤 Add Memo 버튼을 클릭했을 때 50자 이하로 입력해달라는 경고 창이 뜬다.
66 | it("메모 입력 조건(50자 이하 입력)을 지키지 않은 사용자", () => {
67 | fillContent(
68 | `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s`
69 | );
70 | clickAddMemoBtn();
71 |
72 | assertAlertText("글은 50자 이하로 입력해야 합니다.");
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FE POST
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | FE POST
15 | 빠른 프론트엔드 소식
16 |
17 |
18 |
19 | Top News 🏆️
20 |
21 |
22 |
23 |
24 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FE POST
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | FE POST
15 | 빠른 프론트엔드 소식
16 |
17 |
18 |
19 | Top News 🏆️
20 |
21 |
22 |
23 |
24 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FE POST
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | FE POST
15 | 빠른 프론트엔드 소식
16 |
17 |
18 |
19 | Top News 🏆️
20 |
21 |
22 |
23 |
24 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FE POST
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | FE POST
15 | 빠른 프론트엔드 소식
16 |
17 |
18 |
19 | Top News 🏆️
20 |
21 |
22 |
23 |
24 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # Storybook
107 | storybook-static
108 |
--------------------------------------------------------------------------------
/chapter10-board/src/js/main.js:
--------------------------------------------------------------------------------
1 | import { createSpinner } from './spinner.js';
2 |
3 | function createLatestNewsElement(article) {
4 | const { title, link } = article;
5 |
6 | const listItem = document.createElement('li');
7 | const anchor = document.createElement('a');
8 |
9 | anchor.setAttribute('href', link);
10 | anchor.textContent = title;
11 |
12 | listItem.className = 'latest-news-item';
13 | listItem.append(anchor);
14 |
15 | return listItem;
16 | }
17 |
18 | function createTopNewsElement(article) {
19 | const { title, summary, link, thumbnailImage } = article;
20 |
21 | const anchor = document.createElement('a');
22 | anchor.setAttribute('href', link);
23 | anchor.innerHTML = `
24 |
25 |
29 |
30 |

31 |
32 | `;
33 |
34 | return anchor;
35 | }
36 |
37 | function renderTopNews() {
38 | const articleSection = document.getElementById('topNewsList');
39 |
40 | createSpinner(articleSection);
41 |
42 | setTimeout(() => {
43 | fetch('./data/top.json')
44 | .then((res) => res.json())
45 | .then((data) => {
46 | const { articles } = data;
47 | const articleList = articles.map((article) =>
48 | createTopNewsElement(article)
49 | );
50 |
51 | articleSection.append(...articleList);
52 | })
53 | .finally(() => {
54 | hideSpinner(articleSection);
55 | });
56 | }, 1500);
57 | }
58 |
59 | function renderLatestNews() {
60 | const latestNewsList = document.querySelector('.latest-news-list');
61 |
62 | createSpinner(latestNewsList);
63 |
64 | setTimeout(() => {
65 | fetch('./data/latest.json')
66 | .then((res) => res.json())
67 | .then((data) => {
68 | const { articles } = data;
69 | const articleList = articles.map((article) =>
70 | createLatestNewsElement(article)
71 | );
72 |
73 | latestNewsList.append(...articleList);
74 | })
75 | .finally(() => {
76 | hideSpinner(latestNewsList);
77 | });
78 | }, 1500);
79 | }
80 |
81 | function hideSpinner(parent) {
82 | const spinnerArea = parent.querySelector('.spinner-area');
83 | spinnerArea.style.display = 'none';
84 | }
85 |
86 | document.addEventListener('DOMContentLoaded', () => {
87 | renderTopNews();
88 | renderLatestNews();
89 | });
90 |
--------------------------------------------------------------------------------
/chapter11-tools/npm/src/js/main.js:
--------------------------------------------------------------------------------
1 | import { createSpinner } from './spinner.js';
2 |
3 | function createLatestNewsElement(article) {
4 | const { title, link } = article;
5 |
6 | const listItem = document.createElement('li');
7 | const anchor = document.createElement('a');
8 |
9 | anchor.setAttribute('href', link);
10 | anchor.textContent = title;
11 |
12 | listItem.className = 'latest-news-item';
13 | listItem.append(anchor);
14 |
15 | return listItem;
16 | }
17 |
18 | function createTopNewsElement(article) {
19 | const { title, summary, link, thumbnailImage } = article;
20 |
21 | const anchor = document.createElement('a');
22 | anchor.setAttribute('href', link);
23 | anchor.innerHTML = `
24 |
25 |
29 |
30 |

31 |
32 | `;
33 |
34 | return anchor;
35 | }
36 |
37 | function renderTopNews() {
38 | const articleSection = document.getElementById('topNewsList');
39 |
40 | createSpinner(articleSection);
41 |
42 | setTimeout(() => {
43 | fetch('./data/top.json')
44 | .then((res) => res.json())
45 | .then((data) => {
46 | const { articles } = data;
47 | const articleList = articles.map((article) =>
48 | createTopNewsElement(article)
49 | );
50 |
51 | articleSection.append(...articleList);
52 | })
53 | .finally(() => {
54 | hideSpinner(articleSection);
55 | });
56 | }, 1500);
57 | }
58 |
59 | function renderLatestNews() {
60 | const latestNewsList = document.querySelector('.latest-news-list');
61 |
62 | createSpinner(latestNewsList);
63 |
64 | setTimeout(() => {
65 | fetch('./data/latest.json')
66 | .then((res) => res.json())
67 | .then((data) => {
68 | const { articles } = data;
69 | const articleList = articles.map((article) =>
70 | createLatestNewsElement(article)
71 | );
72 |
73 | latestNewsList.append(...articleList);
74 | })
75 | .finally(() => {
76 | hideSpinner(latestNewsList);
77 | });
78 | }, 1500);
79 | }
80 |
81 | function hideSpinner(parent) {
82 | const spinnerArea = parent.querySelector('.spinner-area');
83 | spinnerArea.style.display = 'none';
84 | }
85 |
86 | document.addEventListener('DOMContentLoaded', () => {
87 | renderTopNews();
88 | renderLatestNews();
89 | });
90 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/src/js/main.js:
--------------------------------------------------------------------------------
1 | import { createSpinner } from './spinner.js';
2 | import '../css/main.css';
3 |
4 | function createLatestNewsElement(article) {
5 | const { title, link } = article;
6 |
7 | const listItem = document.createElement('li');
8 | const anchor = document.createElement('a');
9 |
10 | anchor.setAttribute('href', link);
11 | anchor.textContent = title;
12 |
13 | listItem.className = 'latest-news-item';
14 | listItem.append(anchor);
15 |
16 | return listItem;
17 | }
18 |
19 | function createTopNewsElement(article) {
20 | const { title, summary, link, thumbnailImage } = article;
21 |
22 | const anchor = document.createElement('a');
23 | anchor.setAttribute('href', link);
24 | anchor.innerHTML = `
25 |
26 |
30 |
31 |

32 |
33 | `;
34 |
35 | return anchor;
36 | }
37 |
38 | function renderTopNews() {
39 | const articleSection = document.getElementById('topNewsList');
40 |
41 | createSpinner(articleSection);
42 |
43 | setTimeout(() => {
44 | fetch('../../data/top.json')
45 | .then((res) => res.json())
46 | .then((data) => {
47 | const { articles } = data;
48 | const articleList = articles.map((article) =>
49 | createTopNewsElement(article)
50 | );
51 |
52 | articleSection.append(...articleList);
53 | })
54 | .finally(() => {
55 | hideSpinner(articleSection);
56 | });
57 | }, 1500);
58 | }
59 |
60 | function renderLatestNews() {
61 | const latestNewsList = document.querySelector('.latest-news-list');
62 |
63 | createSpinner(latestNewsList);
64 |
65 | setTimeout(() => {
66 | fetch('../../data/latest.json')
67 | .then((res) => res.json())
68 | .then((data) => {
69 | const { articles } = data;
70 | const articleList = articles.map((article) =>
71 | createLatestNewsElement(article)
72 | );
73 |
74 | latestNewsList.append(...articleList);
75 | })
76 | .finally(() => {
77 | hideSpinner(latestNewsList);
78 | });
79 | }, 1500);
80 | }
81 |
82 | function hideSpinner(parent) {
83 | const spinnerArea = parent.querySelector('.spinner-area');
84 | spinnerArea.style.display = 'none';
85 | }
86 |
87 | document.addEventListener('DOMContentLoaded', () => {
88 | renderTopNews();
89 | renderLatestNews();
90 | });
91 |
--------------------------------------------------------------------------------
/chapter10-board/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FE POST
5 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 | FE POST
22 | 빠른 프론트엔드 소식
23 |
24 |
25 |
26 | Top News 🏆️
27 |
28 |
29 |
30 |
31 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/chapter11-tools/npm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FE POST
5 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 | FE POST
22 | 빠른 프론트엔드 소식
23 |
24 |
25 |
26 | Top News 🏆️
27 |
28 |
29 |
30 |
31 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/js/main.js:
--------------------------------------------------------------------------------
1 | import { fetch } from 'whatwg-fetch';
2 | import { createSpinner } from './spinner.js';
3 | import './polyfill/append.js';
4 | import '../scss/index.scss';
5 |
6 | function createLatestNewsElement(article) {
7 | const { title, link } = article;
8 |
9 | const listItem = document.createElement('li');
10 | const anchor = document.createElement('a');
11 |
12 | anchor.setAttribute('href', link);
13 | anchor.textContent = title;
14 |
15 | listItem.className = 'latest-news-item';
16 | listItem.append(anchor);
17 |
18 | return listItem;
19 | }
20 |
21 | function createTopNewsElement(article) {
22 | const { title, summary, link, thumbnailImage } = article;
23 |
24 | const anchor = document.createElement('a');
25 | anchor.setAttribute('href', link);
26 | anchor.innerHTML = `
27 |
28 |
32 |
33 |

34 |
35 | `;
36 |
37 | return anchor;
38 | }
39 |
40 | function renderTopNews() {
41 | const articleSection= document.getElementById('topNewsList');
42 |
43 | createSpinner(articleSection);
44 |
45 | setTimeout(() => {
46 | fetch('../../data/top.json')
47 | .then((res) => res.json())
48 | .then((data) => {
49 | const { articles } = data;
50 | const articleList = articles.map((article) => createTopNewsElement(article));
51 |
52 | articleSection.append(...articleList);
53 | })
54 | .finally(() => {
55 | hideSpinner(articleSection);
56 | });
57 | }, 1500);
58 | }
59 |
60 | function renderLatestNews() {
61 | const latestNewsList = document.querySelector('.latest-news-list');
62 |
63 | createSpinner(latestNewsList);
64 |
65 | setTimeout(() => {
66 | fetch('../../data/latest.json')
67 | .then((res) => res.json())
68 | .then((data) => {
69 | const { articles } = data;
70 | const articleList = articles.map((article) => createLatestNewsElement(article));
71 |
72 | latestNewsList.append(...articleList);
73 | })
74 | .finally(() => {
75 | hideSpinner(latestNewsList);
76 | });
77 | }, 1500);
78 | }
79 |
80 | function hideSpinner(parent) {
81 | const spinnerArea = parent.querySelector('.spinner-area');
82 | spinnerArea.style.display = 'none';
83 | }
84 |
85 | document.addEventListener('DOMContentLoaded', () => {
86 | renderTopNews();
87 | renderLatestNews();
88 | });
89 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/scss/_flex.scss:
--------------------------------------------------------------------------------
1 | @mixin flexbox {
2 | display: -webkit-box;
3 | display: -moz-box;
4 | display: -ms-flexbox;
5 | display: -webkit-flex;
6 | display: flex;
7 | }
8 |
9 | @mixin flex-direction($direction: row) {
10 | @if $direction == row-reverse {
11 | -webkit-box-direction: reverse;
12 | -webkit-box-orient: horizontal;
13 | -moz-box-direction: reverse;
14 | -moz-box-orient: horizontal;
15 | } @else if $direction == column {
16 | -webkit-box-direction: normal;
17 | -webkit-box-orient: vertical;
18 | -moz-box-direction: normal;
19 | -moz-box-orient: vertical;
20 | } @else if $direction == column-reverse {
21 | -webkit-box-direction: reverse;
22 | -webkit-box-orient: vertical;
23 | -moz-box-direction: reverse;
24 | -moz-box-orient: vertical;
25 | } @else {
26 | -webkit-box-direction: normal;
27 | -webkit-box-orient: horizontal;
28 | -moz-box-direction: normal;
29 | -moz-box-orient: horizontal;
30 | }
31 |
32 | -webkit-flex-direction: $direction;
33 | -ms-flex-direction: $direction;
34 | flex-direction: $direction;
35 | }
36 |
37 | @mixin flex-grow($int: 1) {
38 | -webkit-box-flex: $int;
39 | -moz-box-flex: $int;
40 | -webkit-flex-grow: $int;
41 | -ms-flex: $int;
42 | flex-grow: $int;
43 | }
44 |
45 | @mixin flex-basis($value: auto) {
46 | -webkit-flex-basis: $value;
47 | flex-basis: $value;
48 | }
49 |
50 | @mixin justify-content($value: flex-start) {
51 | @if $value == flex-start {
52 | -webkit-box-pack: start;
53 | -moz-box-pack: start;
54 | -ms-flex-pack: start;
55 | } @else if $value == flex-end {
56 | -webkit-box-pack: end;
57 | -moz-box-pack: end;
58 | -ms-flex-pack: end;
59 | } @else if $value == space-between {
60 | -webkit-box-pack: justify;
61 | -moz-box-pack: justify;
62 | -ms-flex-pack: justify;
63 | } @else if $value == space-around {
64 | -ms-flex-pack: distribute;
65 | } @else {
66 | -webkit-box-pack: $value;
67 | -moz-box-pack: $value;
68 | -ms-flex-pack: $value;
69 | }
70 |
71 | -webkit-justify-content: $value;
72 | justify-content: $value;
73 | }
74 |
75 | @mixin align-items($value: stretch) {
76 | @if $value == flex-start {
77 | -webkit-box-align: start;
78 | -moz-box-align: start;
79 | -ms-flex-align: start;
80 | } @else if $value == flex-end {
81 | -webkit-box-align: end;
82 | -moz-box-align: end;
83 | -ms-flex-align: end;
84 | } @else {
85 | -webkit-box-align: $value;
86 | -moz-box-align: $value;
87 | -ms-flex-align: $value;
88 | }
89 |
90 | -webkit-align-items: $value;
91 | align-items: $value;
92 | }
93 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/scss/_flex.scss:
--------------------------------------------------------------------------------
1 | @mixin flexbox {
2 | display: -webkit-box;
3 | display: -moz-box;
4 | display: -ms-flexbox;
5 | display: -webkit-flex;
6 | display: flex;
7 | }
8 |
9 | @mixin flex-direction($direction: row) {
10 | @if $direction == row-reverse {
11 | -webkit-box-direction: reverse;
12 | -webkit-box-orient: horizontal;
13 | -moz-box-direction: reverse;
14 | -moz-box-orient: horizontal;
15 | } @else if $direction == column {
16 | -webkit-box-direction: normal;
17 | -webkit-box-orient: vertical;
18 | -moz-box-direction: normal;
19 | -moz-box-orient: vertical;
20 | } @else if $direction == column-reverse {
21 | -webkit-box-direction: reverse;
22 | -webkit-box-orient: vertical;
23 | -moz-box-direction: reverse;
24 | -moz-box-orient: vertical;
25 | } @else {
26 | -webkit-box-direction: normal;
27 | -webkit-box-orient: horizontal;
28 | -moz-box-direction: normal;
29 | -moz-box-orient: horizontal;
30 | }
31 |
32 | -webkit-flex-direction: $direction;
33 | -ms-flex-direction: $direction;
34 | flex-direction: $direction;
35 | }
36 |
37 | @mixin flex-grow($int: 1) {
38 | -webkit-box-flex: $int;
39 | -moz-box-flex: $int;
40 | -webkit-flex-grow: $int;
41 | -ms-flex: $int;
42 | flex-grow: $int;
43 | }
44 |
45 | @mixin flex-basis($value: auto) {
46 | -webkit-flex-basis: $value;
47 | flex-basis: $value;
48 | }
49 |
50 | @mixin justify-content($value: flex-start) {
51 | @if $value == flex-start {
52 | -webkit-box-pack: start;
53 | -moz-box-pack: start;
54 | -ms-flex-pack: start;
55 | } @else if $value == flex-end {
56 | -webkit-box-pack: end;
57 | -moz-box-pack: end;
58 | -ms-flex-pack: end;
59 | } @else if $value == space-between {
60 | -webkit-box-pack: justify;
61 | -moz-box-pack: justify;
62 | -ms-flex-pack: justify;
63 | } @else if $value == space-around {
64 | -ms-flex-pack: distribute;
65 | } @else {
66 | -webkit-box-pack: $value;
67 | -moz-box-pack: $value;
68 | -ms-flex-pack: $value;
69 | }
70 |
71 | -webkit-justify-content: $value;
72 | justify-content: $value;
73 | }
74 |
75 | @mixin align-items($value: stretch) {
76 | @if $value == flex-start {
77 | -webkit-box-align: start;
78 | -moz-box-align: start;
79 | -ms-flex-align: start;
80 | } @else if $value == flex-end {
81 | -webkit-box-align: end;
82 | -moz-box-align: end;
83 | -ms-flex-align: end;
84 | } @else {
85 | -webkit-box-align: $value;
86 | -moz-box-align: $value;
87 | -ms-flex-align: $value;
88 | }
89 |
90 | -webkit-align-items: $value;
91 | align-items: $value;
92 | }
93 |
--------------------------------------------------------------------------------
/chapter11-tools/babel/src/js/main.js:
--------------------------------------------------------------------------------
1 | import { fetch } from 'whatwg-fetch';
2 | import { createSpinner } from './spinner.js';
3 | import './polyfill/append';
4 | import '../css/main.css';
5 |
6 | function createLatestNewsElement(article) {
7 | const { title, link } = article;
8 |
9 | const listItem = document.createElement('li');
10 | const anchor = document.createElement('a');
11 |
12 | anchor.setAttribute('href', link);
13 | anchor.textContent = title;
14 |
15 | listItem.className = 'latest-news-item';
16 | listItem.append(anchor);
17 |
18 | return listItem;
19 | }
20 |
21 | function createTopNewsElement(article) {
22 | const { title, summary, link, thumbnailImage } = article;
23 |
24 | const anchor = document.createElement('a');
25 | anchor.setAttribute('href', link);
26 | anchor.innerHTML = `
27 |
28 |
32 |
33 |

34 |
35 | `;
36 |
37 | return anchor;
38 | }
39 |
40 | function renderTopNews() {
41 | const articleSection = document.getElementById('topNewsList');
42 |
43 | createSpinner(articleSection);
44 |
45 | setTimeout(() => {
46 | fetch('../../data/top.json')
47 | .then((res) => res.json())
48 | .then((data) => {
49 | const { articles } = data;
50 | const articleList = articles.map((article) =>
51 | createTopNewsElement(article)
52 | );
53 |
54 | articleSection.append(...articleList);
55 | })
56 | .finally(() => {
57 | hideSpinner(articleSection);
58 | });
59 | }, 1500);
60 | }
61 |
62 | function renderLatestNews() {
63 | const latestNewsList = document.querySelector('.latest-news-list');
64 |
65 | createSpinner(latestNewsList);
66 |
67 | setTimeout(() => {
68 | fetch('../../data/latest.json')
69 | .then((res) => res.json())
70 | .then((data) => {
71 | const { articles } = data;
72 | const articleList = articles.map((article) =>
73 | createLatestNewsElement(article)
74 | );
75 |
76 | latestNewsList.append(...articleList);
77 | })
78 | .finally(() => {
79 | hideSpinner(latestNewsList);
80 | });
81 | }, 1500);
82 | }
83 |
84 | function hideSpinner(parent) {
85 | const spinnerArea = parent.querySelector('.spinner-area');
86 | spinnerArea.style.display = 'none';
87 | }
88 |
89 | document.addEventListener('DOMContentLoaded', () => {
90 | renderTopNews();
91 | renderLatestNews();
92 | });
93 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/js/main.js:
--------------------------------------------------------------------------------
1 | import { fetch } from 'whatwg-fetch';
2 | import { createSpinner } from './spinner.js';
3 | import './polyfill/append.js';
4 | import '../scss/index.scss';
5 |
6 | function createLatestNewsElement(article) {
7 | const { title, link } = article;
8 |
9 | const listItem = document.createElement('li');
10 | const anchor = document.createElement('a');
11 |
12 | anchor.setAttribute('href', link);
13 | anchor.textContent = title;
14 |
15 | listItem.className = 'latest-news-item';
16 | listItem.append(anchor);
17 |
18 | return listItem;
19 | }
20 |
21 | function createTopNewsElement(article) {
22 | const { title, summary, link, thumbnailImage } = article;
23 |
24 | const anchor = document.createElement('a');
25 | anchor.setAttribute('href', link);
26 | anchor.innerHTML = `
27 |
28 |
32 |
33 |

34 |
35 | `;
36 |
37 | return anchor;
38 | }
39 |
40 | function renderTopNews() {
41 | const articleSection = document.getElementById('topNewsList');
42 |
43 | createSpinner(articleSection);
44 |
45 | setTimeout(() => {
46 | fetch('../../data/top.json')
47 | .then((res) => res.json())
48 | .then((data) => {
49 | const { articles } = data;
50 | const articleList = articles.map((article) =>
51 | createTopNewsElement(article)
52 | );
53 |
54 | articleSection.append(...articleList);
55 | })
56 | .finally(() => {
57 | hideSpinner(articleSection);
58 | });
59 | }, 1500);
60 | }
61 |
62 | function renderLatestNews() {
63 | const latestNewsList = document.querySelector('.latest-news-list');
64 |
65 | createSpinner(latestNewsList);
66 |
67 | setTimeout(() => {
68 | fetch('../../data/latest.json')
69 | .then((res) => res.json())
70 | .then((data) => {
71 | const { articles } = data;
72 | const articleList = articles.map((article) =>
73 | createLatestNewsElement(article)
74 | );
75 |
76 | latestNewsList.append(...articleList);
77 | })
78 | .finally(() => {
79 | hideSpinner(latestNewsList);
80 | });
81 | }, 1500);
82 | }
83 |
84 | function hideSpinner(parent) {
85 | const spinnerArea = parent.querySelector('.spinner-area');
86 | spinnerArea.style.display = 'none';
87 | }
88 |
89 | document.addEventListener('DOMContentLoaded', () => {
90 | renderTopNews();
91 | renderLatestNews();
92 | });
93 |
--------------------------------------------------------------------------------
/chapter10-board/src/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | .content {
19 | display: flex;
20 | max-width: 1280px;
21 | margin: auto;
22 | padding: 50px;
23 | color: #faebd7;
24 | }
25 |
26 | .content .top-news-section {
27 | flex-basis: 75%;
28 | padding: 10px;
29 | }
30 |
31 | .content .top-news-section h2 {
32 | color: #efbb35;
33 | font-size: 36px;
34 | }
35 |
36 | .content aside {
37 | flex-basis: 25%;
38 | margin: 10px;
39 | padding: 20px;
40 | border-left: 1px solid #5a6268;
41 | }
42 |
43 | .latest-section .latest-news-list {
44 | margin-top: 10px;
45 | }
46 |
47 | .latest-section .latest-news-item {
48 | padding: 10px 0;
49 | border-bottom: 1px dotted #5a6268;
50 | }
51 |
52 | .latest-section .latest-news-item:hover {
53 | color: #add8e6;
54 | }
55 |
56 | .support-section {
57 | margin-top: 20px;
58 | }
59 |
60 | .github-support {
61 | margin-top: 10px;
62 | font-size: 14px;
63 | font-weight: 600;
64 | }
65 |
66 | .github-support a {
67 | line-height: 33px;
68 | padding: 7px 12px;
69 | background-color: #30363d;
70 | border: 1px solid #5a6268;
71 | border-radius: 3px;
72 | cursor: pointer;
73 | }
74 |
75 | .github-support a:hover {
76 | border-color: #8b949e;
77 | }
78 |
79 | .github-support .heart {
80 | vertical-align: text-top;
81 | color: #ea4aaa;
82 | fill: currentColor;
83 | }
84 |
85 | .news {
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | border-bottom: 1px solid #5a6268;
90 | min-height: 200px;
91 | padding: 20px 10px;
92 | }
93 |
94 | .information .title {
95 | font-size: 25px;
96 | }
97 |
98 | .information .title:hover {
99 | color: #add8e6;
100 | }
101 |
102 | .information .description {
103 | margin-top: 15px;
104 | font-size: 18px;
105 | }
106 |
107 | .thumbnail-area {
108 | max-width: 140px;
109 | }
110 |
111 | .thumbnail-area .thumbnail {
112 | max-width: 100%;
113 | height: auto;
114 | }
115 |
116 | header {
117 | color: #7d99a3;
118 | text-align: center;
119 | padding: 50px;
120 | }
121 |
122 | header h1 {
123 | font-style: italic;
124 | font-weight: bold;
125 | font-size: 50px;
126 | }
127 |
128 | header p {
129 | font-size: 24px;
130 | font-weight: bold;
131 | }
132 |
133 | footer {
134 | text-align: center;
135 | padding-bottom: 50px;
136 | color: #faebd7;
137 | }
138 |
139 | .spinner-area {
140 | display: flex;
141 | min-height: 400px;
142 | justify-content: center;
143 | align-items: center;
144 | }
145 |
146 | .spinner-area img {
147 | width: 50px;
148 | }
149 |
150 | @media only screen and (max-width: 768px) {
151 | .content {
152 | flex-direction: column;
153 | }
154 | .content aside {
155 | margin: 0;
156 | border-left: none;
157 | border-bottom: 1px solid #5a6268;
158 | }
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 |
164 | @media only screen and (max-width: 375px) {
165 | header {
166 | padding-bottom: 20px;
167 | }
168 | header h1 {
169 | font-size: 24px;
170 | }
171 | header p {
172 | font-size: 20px;
173 | }
174 | .content {
175 | padding: 10px;
176 | font-size: 13px;
177 | }
178 | .content .top-news-section h2 {
179 | font-size: 20px;
180 | }
181 | .news {
182 | min-height: 150px;
183 | }
184 | .information .title {
185 | font-size: 16px;
186 | }
187 | .information .description {
188 | font-size: 13px;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | .content {
19 | display: flex;
20 | max-width: 1280px;
21 | margin: auto;
22 | padding: 50px;
23 | color: #faebd7;
24 | }
25 |
26 | .content .top-news-section {
27 | flex-basis: 75%;
28 | padding: 10px;
29 | }
30 |
31 | .content .top-news-section h2 {
32 | color: #efbb35;
33 | font-size: 36px;
34 | }
35 |
36 | .content aside {
37 | flex-basis: 25%;
38 | margin: 10px;
39 | padding: 20px;
40 | border-left: 1px solid #5a6268;
41 | }
42 |
43 | .latest-section .latest-news-list {
44 | margin-top: 10px;
45 | }
46 |
47 | .latest-section .latest-news-item {
48 | padding: 10px 0;
49 | border-bottom: 1px dotted #5a6268;
50 | }
51 |
52 | .latest-section .latest-news-item:hover {
53 | color: #add8e6;
54 | }
55 |
56 | .support-section {
57 | margin-top: 20px;
58 | }
59 |
60 | .github-support {
61 | margin-top: 10px;
62 | font-size: 14px;
63 | font-weight: 600;
64 | }
65 |
66 | .github-support a {
67 | line-height: 33px;
68 | padding: 7px 12px;
69 | background-color: #30363d;
70 | border: 1px solid #5a6268;
71 | border-radius: 3px;
72 | cursor: pointer;
73 | }
74 |
75 | .github-support a:hover {
76 | border-color: #8b949e;
77 | }
78 |
79 | .github-support .heart {
80 | vertical-align: text-top;
81 | color: #ea4aaa;
82 | fill: currentColor;
83 | }
84 |
85 | .news {
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | border-bottom: 1px solid #5a6268;
90 | min-height: 200px;
91 | padding: 20px 10px;
92 | }
93 |
94 | .information .title {
95 | font-size: 25px;
96 | }
97 |
98 | .information .title:hover {
99 | color: #add8e6;
100 | }
101 |
102 | .information .description {
103 | margin-top: 15px;
104 | font-size: 18px;
105 | }
106 |
107 | .thumbnail-area {
108 | max-width: 140px;
109 | }
110 |
111 | .thumbnail-area .thumbnail {
112 | max-width: 100%;
113 | height: auto;
114 | }
115 |
116 | header {
117 | color: #7d99a3;
118 | text-align: center;
119 | padding: 50px;
120 | }
121 |
122 | header h1 {
123 | font-style: italic;
124 | font-weight: bold;
125 | font-size: 50px;
126 | }
127 |
128 | header p {
129 | font-size: 24px;
130 | font-weight: bold;
131 | }
132 |
133 | footer {
134 | text-align: center;
135 | padding-bottom: 50px;
136 | color: #faebd7;
137 | }
138 |
139 | .spinner-area {
140 | display: flex;
141 | min-height: 400px;
142 | justify-content: center;
143 | align-items: center;
144 | }
145 |
146 | .spinner-area img {
147 | width: 50px;
148 | }
149 |
150 | @media only screen and (max-width: 768px) {
151 | .content {
152 | flex-direction: column;
153 | }
154 | .content aside {
155 | margin: 0;
156 | border-left: none;
157 | border-bottom: 1px solid #5a6268;
158 | }
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 |
164 | @media only screen and (max-width: 375px) {
165 | header {
166 | padding-bottom: 20px;
167 | }
168 | header h1 {
169 | font-size: 24px;
170 | }
171 | header p {
172 | font-size: 20px;
173 | }
174 | .content {
175 | padding: 10px;
176 | font-size: 13px;
177 | }
178 | .content .top-news-section h2 {
179 | font-size: 20px;
180 | }
181 | .news {
182 | min-height: 150px;
183 | }
184 | .information .title {
185 | font-size: 16px;
186 | }
187 | .information .description {
188 | font-size: 13px;
189 | }
190 | }
--------------------------------------------------------------------------------
/chapter11-tools/babel/src/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | .content {
19 | display: flex;
20 | max-width: 1280px;
21 | margin: auto;
22 | padding: 50px;
23 | color: #faebd7;
24 | }
25 |
26 | .content .top-news-section {
27 | flex-basis: 75%;
28 | padding: 10px;
29 | }
30 |
31 | .content .top-news-section h2 {
32 | color: #efbb35;
33 | font-size: 36px;
34 | }
35 |
36 | .content aside {
37 | flex-basis: 25%;
38 | margin: 10px;
39 | padding: 20px;
40 | border-left: 1px solid #5a6268;
41 | }
42 |
43 | .latest-section .latest-news-list {
44 | margin-top: 10px;
45 | }
46 |
47 | .latest-section .latest-news-item {
48 | padding: 10px 0;
49 | border-bottom: 1px dotted #5a6268;
50 | }
51 |
52 | .latest-section .latest-news-item:hover {
53 | color: #add8e6;
54 | }
55 |
56 | .support-section {
57 | margin-top: 20px;
58 | }
59 |
60 | .github-support {
61 | margin-top: 10px;
62 | font-size: 14px;
63 | font-weight: 600;
64 | }
65 |
66 | .github-support a {
67 | line-height: 33px;
68 | padding: 7px 12px;
69 | background-color: #30363d;
70 | border: 1px solid #5a6268;
71 | border-radius: 3px;
72 | cursor: pointer;
73 | }
74 |
75 | .github-support a:hover {
76 | border-color: #8b949e;
77 | }
78 |
79 | .github-support .heart {
80 | vertical-align: text-top;
81 | color: #ea4aaa;
82 | fill: currentColor;
83 | }
84 |
85 | .news {
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | border-bottom: 1px solid #5a6268;
90 | min-height: 200px;
91 | padding: 20px 10px;
92 | }
93 |
94 | .information .title {
95 | font-size: 25px;
96 | }
97 |
98 | .information .title:hover {
99 | color: #add8e6;
100 | }
101 |
102 | .information .description {
103 | margin-top: 15px;
104 | font-size: 18px;
105 | }
106 |
107 | .thumbnail-area {
108 | max-width: 140px;
109 | }
110 |
111 | .thumbnail-area .thumbnail {
112 | max-width: 100%;
113 | height: auto;
114 | }
115 |
116 | header {
117 | color: #7d99a3;
118 | text-align: center;
119 | padding: 50px;
120 | }
121 |
122 | header h1 {
123 | font-style: italic;
124 | font-weight: bold;
125 | font-size: 50px;
126 | }
127 |
128 | header p {
129 | font-size: 24px;
130 | font-weight: bold;
131 | }
132 |
133 | footer {
134 | text-align: center;
135 | padding-bottom: 50px;
136 | color: #faebd7;
137 | }
138 |
139 | .spinner-area {
140 | display: flex;
141 | min-height: 400px;
142 | justify-content: center;
143 | align-items: center;
144 | }
145 |
146 | .spinner-area img {
147 | width: 50px;
148 | }
149 |
150 | @media only screen and (max-width: 768px) {
151 | .content {
152 | flex-direction: column;
153 | }
154 | .content aside {
155 | margin: 0;
156 | border-left: none;
157 | border-bottom: 1px solid #5a6268;
158 | }
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 |
164 | @media only screen and (max-width: 375px) {
165 | header {
166 | padding-bottom: 20px;
167 | }
168 | header h1 {
169 | font-size: 24px;
170 | }
171 | header p {
172 | font-size: 20px;
173 | }
174 | .content {
175 | padding: 10px;
176 | font-size: 13px;
177 | }
178 | .content .top-news-section h2 {
179 | font-size: 20px;
180 | }
181 | .news {
182 | min-height: 150px;
183 | }
184 | .information .title {
185 | font-size: 16px;
186 | }
187 | .information .description {
188 | font-size: 13px;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/chapter11-tools/npm/src/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | .content {
19 | display: flex;
20 | max-width: 1280px;
21 | margin: auto;
22 | padding: 50px;
23 | color: #faebd7;
24 | }
25 |
26 | .content .top-news-section {
27 | flex-basis: 75%;
28 | padding: 10px;
29 | }
30 |
31 | .content .top-news-section h2 {
32 | color: #efbb35;
33 | font-size: 36px;
34 | }
35 |
36 | .content aside {
37 | flex-basis: 25%;
38 | margin: 10px;
39 | padding: 20px;
40 | border-left: 1px solid #5a6268;
41 | }
42 |
43 | .latest-section .latest-news-list {
44 | margin-top: 10px;
45 | }
46 |
47 | .latest-section .latest-news-item {
48 | padding: 10px 0;
49 | border-bottom: 1px dotted #5a6268;
50 | }
51 |
52 | .latest-section .latest-news-item:hover {
53 | color: #add8e6;
54 | }
55 |
56 | .support-section {
57 | margin-top: 20px;
58 | }
59 |
60 | .github-support {
61 | margin-top: 10px;
62 | font-size: 14px;
63 | font-weight: 600;
64 | }
65 |
66 | .github-support a {
67 | line-height: 33px;
68 | padding: 7px 12px;
69 | background-color: #30363d;
70 | border: 1px solid #5a6268;
71 | border-radius: 3px;
72 | cursor: pointer;
73 | }
74 |
75 | .github-support a:hover {
76 | border-color: #8b949e;
77 | }
78 |
79 | .github-support .heart {
80 | vertical-align: text-top;
81 | color: #ea4aaa;
82 | fill: currentColor;
83 | }
84 |
85 | .news {
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | border-bottom: 1px solid #5a6268;
90 | min-height: 200px;
91 | padding: 20px 10px;
92 | }
93 |
94 | .information .title {
95 | font-size: 25px;
96 | }
97 |
98 | .information .title:hover {
99 | color: #add8e6;
100 | }
101 |
102 | .information .description {
103 | margin-top: 15px;
104 | font-size: 18px;
105 | }
106 |
107 | .thumbnail-area {
108 | max-width: 140px;
109 | }
110 |
111 | .thumbnail-area .thumbnail {
112 | max-width: 100%;
113 | height: auto;
114 | }
115 |
116 | header {
117 | color: #7d99a3;
118 | text-align: center;
119 | padding: 50px;
120 | }
121 |
122 | header h1 {
123 | font-style: italic;
124 | font-weight: bold;
125 | font-size: 50px;
126 | }
127 |
128 | header p {
129 | font-size: 24px;
130 | font-weight: bold;
131 | }
132 |
133 | footer {
134 | text-align: center;
135 | padding-bottom: 50px;
136 | color: #faebd7;
137 | }
138 |
139 | .spinner-area {
140 | display: flex;
141 | min-height: 400px;
142 | justify-content: center;
143 | align-items: center;
144 | }
145 |
146 | .spinner-area img {
147 | width: 50px;
148 | }
149 |
150 | @media only screen and (max-width: 768px) {
151 | .content {
152 | flex-direction: column;
153 | }
154 | .content aside {
155 | margin: 0;
156 | border-left: none;
157 | border-bottom: 1px solid #5a6268;
158 | }
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 |
164 | @media only screen and (max-width: 375px) {
165 | header {
166 | padding-bottom: 20px;
167 | }
168 | header h1 {
169 | font-size: 24px;
170 | }
171 | header p {
172 | font-size: 20px;
173 | }
174 | .content {
175 | padding: 10px;
176 | font-size: 13px;
177 | }
178 | .content .top-news-section h2 {
179 | font-size: 20px;
180 | }
181 | .news {
182 | min-height: 150px;
183 | }
184 | .information .title {
185 | font-size: 16px;
186 | }
187 | .information .description {
188 | font-size: 13px;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | .content {
19 | display: flex;
20 | max-width: 1280px;
21 | margin: auto;
22 | padding: 50px;
23 | color: #faebd7;
24 | }
25 |
26 | .content .top-news-section {
27 | flex-basis: 75%;
28 | padding: 10px;
29 | }
30 |
31 | .content .top-news-section h2 {
32 | color: #efbb35;
33 | font-size: 36px;
34 | }
35 |
36 | .content aside {
37 | flex-basis: 25%;
38 | margin: 10px;
39 | padding: 20px;
40 | border-left: 1px solid #5a6268;
41 | }
42 |
43 | .latest-section .latest-news-list {
44 | margin-top: 10px;
45 | }
46 |
47 | .latest-section .latest-news-item {
48 | padding: 10px 0;
49 | border-bottom: 1px dotted #5a6268;
50 | }
51 |
52 | .latest-section .latest-news-item:hover {
53 | color: #add8e6;
54 | }
55 |
56 | .support-section {
57 | margin-top: 20px;
58 | }
59 |
60 | .github-support {
61 | margin-top: 10px;
62 | font-size: 14px;
63 | font-weight: 600;
64 | }
65 |
66 | .github-support a {
67 | line-height: 33px;
68 | padding: 7px 12px;
69 | background-color: #30363d;
70 | border: 1px solid #5a6268;
71 | border-radius: 3px;
72 | cursor: pointer;
73 | }
74 |
75 | .github-support a:hover {
76 | border-color: #8b949e;
77 | }
78 |
79 | .github-support .heart {
80 | vertical-align: text-top;
81 | color: #ea4aaa;
82 | fill: currentColor;
83 | }
84 |
85 | .news {
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | border-bottom: 1px solid #5a6268;
90 | min-height: 200px;
91 | padding: 20px 10px;
92 | }
93 |
94 | .information .title {
95 | font-size: 25px;
96 | }
97 |
98 | .information .title:hover {
99 | color: #add8e6;
100 | }
101 |
102 | .information .description {
103 | margin-top: 15px;
104 | font-size: 18px;
105 | }
106 |
107 | .thumbnail-area {
108 | max-width: 140px;
109 | }
110 |
111 | .thumbnail-area .thumbnail {
112 | max-width: 100%;
113 | height: auto;
114 | }
115 |
116 | header {
117 | color: #7d99a3;
118 | text-align: center;
119 | padding: 50px;
120 | }
121 |
122 | header h1 {
123 | font-style: italic;
124 | font-weight: bold;
125 | font-size: 50px;
126 | }
127 |
128 | header p {
129 | font-size: 24px;
130 | font-weight: bold;
131 | }
132 |
133 | footer {
134 | text-align: center;
135 | padding-bottom: 50px;
136 | color: #faebd7;
137 | }
138 |
139 | .spinner-area {
140 | display: flex;
141 | min-height: 400px;
142 | justify-content: center;
143 | align-items: center;
144 | }
145 |
146 | .spinner-area img {
147 | width: 50px;
148 | }
149 |
150 | @media only screen and (max-width: 768px) {
151 | .content {
152 | flex-direction: column;
153 | }
154 | .content aside {
155 | margin: 0;
156 | border-left: none;
157 | border-bottom: 1px solid #5a6268;
158 | }
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 |
164 | @media only screen and (max-width: 375px) {
165 | header {
166 | padding-bottom: 20px;
167 | }
168 | header h1 {
169 | font-size: 24px;
170 | }
171 | header p {
172 | font-size: 20px;
173 | }
174 | .content {
175 | padding: 10px;
176 | font-size: 13px;
177 | }
178 | .content .top-news-section h2 {
179 | font-size: 20px;
180 | }
181 | .news {
182 | min-height: 150px;
183 | }
184 | .information .title {
185 | font-size: 16px;
186 | }
187 | .information .description {
188 | font-size: 13px;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/chapter11-tools/bundler/src/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style: none;
6 | }
7 |
8 | body {
9 | background: #002b36;
10 | font-family: sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | .content {
19 | display: flex;
20 | max-width: 1280px;
21 | margin: auto;
22 | padding: 50px;
23 | color: #faebd7;
24 | }
25 |
26 | .content .top-news-section {
27 | flex-basis: 75%;
28 | padding: 10px;
29 | }
30 |
31 | .content .top-news-section h2 {
32 | color: #efbb35;
33 | font-size: 36px;
34 | }
35 |
36 | .content aside {
37 | flex-basis: 25%;
38 | margin: 10px;
39 | padding: 20px;
40 | border-left: 1px solid #5a6268;
41 | }
42 |
43 | .latest-section .latest-news-list {
44 | margin-top: 10px;
45 | }
46 |
47 | .latest-section .latest-news-item {
48 | padding: 10px 0;
49 | border-bottom: 1px dotted #5a6268;
50 | }
51 |
52 | .latest-section .latest-news-item:hover {
53 | color: #add8e6;
54 | }
55 |
56 | .support-section {
57 | margin-top: 20px;
58 | }
59 |
60 | .github-support {
61 | margin-top: 10px;
62 | font-size: 14px;
63 | font-weight: 600;
64 | }
65 |
66 | .github-support a {
67 | line-height: 33px;
68 | padding: 7px 12px;
69 | background-color: #30363d;
70 | border: 1px solid #5a6268;
71 | border-radius: 3px;
72 | cursor: pointer;
73 | }
74 |
75 | .github-support a:hover {
76 | border-color: #8b949e;
77 | }
78 |
79 | .github-support .heart {
80 | vertical-align: text-top;
81 | color: #ea4aaa;
82 | fill: currentColor;
83 | }
84 |
85 | .news {
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | border-bottom: 1px solid #5a6268;
90 | min-height: 200px;
91 | padding: 20px 10px;
92 | }
93 |
94 | .information .title {
95 | font-size: 25px;
96 | }
97 |
98 | .information .title:hover {
99 | color: #add8e6;
100 | }
101 |
102 | .information .description {
103 | margin-top: 15px;
104 | font-size: 18px;
105 | }
106 |
107 | .thumbnail-area {
108 | max-width: 140px;
109 | }
110 |
111 | .thumbnail-area .thumbnail {
112 | max-width: 100%;
113 | height: auto;
114 | }
115 |
116 | header {
117 | color: #7d99a3;
118 | text-align: center;
119 | padding: 50px;
120 | }
121 |
122 | header h1 {
123 | font-style: italic;
124 | font-weight: bold;
125 | font-size: 50px;
126 | }
127 |
128 | header p {
129 | font-size: 24px;
130 | font-weight: bold;
131 | }
132 |
133 | footer {
134 | text-align: center;
135 | padding-bottom: 50px;
136 | color: #faebd7;
137 | }
138 |
139 | .spinner-area {
140 | display: flex;
141 | min-height: 400px;
142 | justify-content: center;
143 | align-items: center;
144 | }
145 |
146 | .spinner-area img {
147 | width: 50px;
148 | }
149 |
150 | @media only screen and (max-width: 768px) {
151 | .content {
152 | flex-direction: column;
153 | }
154 | .content aside {
155 | margin: 0;
156 | border-left: none;
157 | border-bottom: 1px solid #5a6268;
158 | }
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 |
164 | @media only screen and (max-width: 375px) {
165 | header {
166 | padding-bottom: 20px;
167 | }
168 | header h1 {
169 | font-size: 24px;
170 | }
171 | header p {
172 | font-size: 20px;
173 | }
174 | .content {
175 | padding: 10px;
176 | font-size: 13px;
177 | }
178 | .content .top-news-section h2 {
179 | font-size: 20px;
180 | }
181 | .news {
182 | min-height: 150px;
183 | }
184 | .information .title {
185 | font-size: 16px;
186 | }
187 | .information .description {
188 | font-size: 13px;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/chapter11-tools/sass/src/scss/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'vars';
2 | @use 'flex';
3 |
4 | $hovered-color: #add8e6;
5 |
6 | @function get-added-default-margin($target) {
7 | @return vars.$margin + $target;
8 | }
9 |
10 | .content {
11 | @include flex.flexbox;
12 |
13 | max-width: 1280px;
14 | margin: auto;
15 | padding: 50px;
16 | color: vars.$color;
17 |
18 | .top-news-section {
19 | @include flex.flex-basis(75%);
20 |
21 | padding: vars.$padding;
22 |
23 | h2 {
24 | color: #efbb35;
25 | font-size: 36px;
26 | }
27 | }
28 |
29 | aside {
30 | @include flex.flexbox;
31 | @include flex.flex-basis(25%);
32 | @include flex.flex-direction(column);
33 |
34 | padding: 20px;
35 | margin: vars.$margin;
36 | border-left: vars.$border;
37 |
38 | .latest-news-list {
39 | margin-top: vars.$margin;
40 | }
41 |
42 | .latest-news-item {
43 | padding: vars.$padding 0;
44 | border-bottom: 1px dotted #5a6268;
45 |
46 | &:hover {
47 | color: $hovered-color;
48 | }
49 | }
50 | }
51 | }
52 |
53 | .news {
54 | @include flex.flexbox;
55 | @include flex.justify-content(space-between);
56 | @include flex.align-items(center);
57 |
58 | border-bottom: vars.$border;
59 | min-height: 200px;
60 | padding: 20px 10px;
61 |
62 | .information {
63 | .title {
64 | font-size: 25px;
65 |
66 | &:hover {
67 | color: $hovered-color;
68 | }
69 | }
70 |
71 | .description {
72 | margin-top: get-added-default-margin(5);
73 | font-size: 18px;
74 | }
75 | }
76 |
77 | .thumbnail-area {
78 | max-width: 140px;
79 |
80 | .thumbnail {
81 | max-width: 100%;
82 | height: auto;
83 | }
84 | }
85 | }
86 |
87 | .support-section {
88 | margin-top: get-added-default-margin(10);
89 |
90 | .github-support {
91 | margin-top: vars.$margin;
92 | font: {
93 | size: 14px;
94 | weight: 600;
95 | }
96 |
97 | a {
98 | line-height: 33px;
99 | padding: 7px 12px;
100 | background-color: #30363d;
101 | border: vars.$border;
102 | border-radius: 3px;
103 | cursor: pointer;
104 |
105 | &:hover {
106 | border-color: #8b949e;
107 | }
108 | }
109 |
110 | .heart {
111 | vertical-align: text-top;
112 | color: #ea4aaa;
113 | fill: currentColor;
114 | }
115 | }
116 | }
117 |
118 | header {
119 | color: #7d99a3;
120 | padding: 50px;
121 | text-align: center;
122 |
123 | h1 {
124 | font: {
125 | style: italic;
126 | weight: bold;
127 | size: 50px;
128 | }
129 | }
130 |
131 | p {
132 | font: {
133 | weight: bold;
134 | size: 24px;
135 | }
136 | }
137 | }
138 |
139 | footer {
140 | @include flex.flexbox;
141 | @include flex.justify-content(center);
142 |
143 | padding-bottom: 50px;
144 | color: vars.$color;
145 | }
146 |
147 | @media only screen and (max-width: vars.$tablet-width) {
148 | .content {
149 | flex-direction: column;
150 |
151 | aside {
152 | margin: 0;
153 | border-left: none;
154 | border-bottom: vars.$border;
155 | }
156 |
157 | .thumbnail-area {
158 | display: none;
159 | }
160 | }
161 | }
162 |
163 | @media only screen and (max-width: vars.$mobile-width) {
164 | header {
165 | padding-bottom: 20px;
166 |
167 | h1 {
168 | font-size: 24px;
169 | }
170 |
171 | p {
172 | font-size: 20px;
173 | }
174 | }
175 |
176 | .content {
177 | padding: vars.$padding;
178 | font-size: vars.$mobile-font-basic;
179 |
180 | .top-news-section h2 {
181 | font-size: 20px;
182 | }
183 |
184 | .news {
185 | min-height: 150px;
186 |
187 | .information .title {
188 | font-size: 16px;
189 | }
190 | .information .description {
191 | font-size: vars.$mobile-font-basic;
192 | }
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/chapter11-tools/lint/src/scss/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'vars';
2 | @use 'flex';
3 |
4 | $hovered-color: #add8e6;
5 |
6 | @function get-added-default-margin($target) {
7 | @return vars.$margin + $target;
8 | }
9 |
10 | .content {
11 | @include flex.flexbox;
12 |
13 | max-width: 1280px;
14 | margin: auto;
15 | padding: 50px;
16 | color: vars.$color;
17 |
18 | .top-news-section {
19 | @include flex.flex-basis(75%);
20 |
21 | padding: vars.$padding;
22 |
23 | h2 {
24 | color: #efbb35;
25 | font-size: 36px;
26 | }
27 | }
28 |
29 | aside {
30 | @include flex.flexbox;
31 | @include flex.flex-basis(25%);
32 | @include flex.flex-direction(column);
33 |
34 | padding: 20px;
35 | margin: vars.$margin;
36 | border-left: vars.$border;
37 |
38 | .latest-news-list {
39 | margin-top: vars.$margin;
40 | }
41 |
42 | .latest-news-item {
43 | padding: vars.$padding 0;
44 | border-bottom: 1px dotted #5a6268;
45 |
46 | &:hover {
47 | color: $hovered-color
48 | }
49 | }
50 | }
51 | }
52 |
53 | .news {
54 | @include flex.flexbox;
55 | @include flex.justify-content(space-between);
56 | @include flex.align-items(center);
57 |
58 | border-bottom: vars.$border;
59 | min-height: 200px;
60 | padding: 20px 10px;
61 |
62 | .information {
63 | .title {
64 | font-size: 25px;
65 |
66 | &:hover {
67 | color: $hovered-color;
68 | }
69 | }
70 |
71 | .description {
72 | margin-top: get-added-default-margin(5);
73 | font-size: 18px;
74 | }
75 | }
76 |
77 | .thumbnail-area {
78 | max-width: 140px;
79 |
80 | .thumbnail {
81 | max-width: 100%;
82 | height: auto;
83 | }
84 | }
85 | }
86 |
87 |
88 | .support-section {
89 | margin-top: get-added-default-margin(10);
90 |
91 | .github-support {
92 | margin-top: vars.$margin;
93 | font: {
94 | size: 14px;
95 | weight: 600;
96 | }
97 |
98 | a {
99 | line-height: 33px;
100 | padding: 7px 12px;
101 | background-color: #30363d;
102 | border: vars.$border;
103 | border-radius: 3px;
104 | cursor: pointer;
105 |
106 | &:hover {
107 | border-color: #8b949e;
108 | }
109 | }
110 |
111 | .heart {
112 | vertical-align: text-top;
113 | color: #ea4aaa;
114 | fill: currentColor;
115 | }
116 |
117 | }
118 | }
119 |
120 | header {
121 | color: #7d99a3;
122 | padding: 50px;
123 | text-align: center;
124 |
125 | h1 {
126 | font: {
127 | style: italic;
128 | weight: bold;
129 | size: 50px;
130 | }
131 | }
132 |
133 | p {
134 | font: {
135 | weight: bold;
136 | size: 24px;
137 | }
138 | }
139 | }
140 |
141 | footer {
142 | @include flex.flexbox;
143 | @include flex.justify-content(center);
144 |
145 | padding-bottom: 50px;
146 | color: vars.$color;
147 | }
148 |
149 | @media only screen and (max-width: vars.$tablet-width) {
150 | .content {
151 | flex-direction: column;
152 |
153 | aside {
154 | margin: 0;
155 | border-left: none;
156 | border-bottom: vars.$border;
157 | }
158 |
159 | .thumbnail-area {
160 | display: none;
161 | }
162 | }
163 | }
164 |
165 | @media only screen and (max-width: vars.$mobile-width) {
166 | header {
167 | padding-bottom: 20px;
168 |
169 | h1 {
170 | font-size: 24px;
171 | }
172 |
173 | p {
174 | font-size: 20px;
175 | }
176 | }
177 |
178 | .content {
179 | padding: vars.$padding;
180 | font-size: vars.$mobile-font-basic;
181 |
182 | .top-news-section h2 {
183 | font-size: 20px;
184 | }
185 |
186 | .news {
187 | min-height: 150px;
188 |
189 | .information .title {
190 | font-size: 16px;
191 | }
192 | .information .description {
193 | font-size: vars.$mobile-font-basic;
194 | }
195 | }
196 | }
197 | }
--------------------------------------------------------------------------------