├── .devcontainer └── devcontainer.json ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── 01_01b ├── data.js ├── index.html ├── script.js └── style.css ├── 01_01e ├── data.js ├── index.html ├── script.js └── style.css ├── 01_02b ├── data.js ├── index.html ├── script.js └── style.css ├── 01_02e ├── data.js ├── index.html ├── script.js └── style.css ├── 01_03b ├── components │ ├── Card.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js └── style.css ├── 01_03e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js └── style.css ├── 01_04b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js └── style.css ├── 01_04e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js └── style.css ├── 01_05b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js └── style.css ├── 01_05e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js └── style.css ├── 02_02b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js ├── style.css └── toggle.css ├── 02_02e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js ├── style.css └── toggle.css ├── 02_03b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js ├── style.css └── toggle.css ├── 02_03e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js ├── style.css └── toggle.css ├── 02_04b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js ├── style.css └── toggle.css ├── 02_04e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── script.js ├── style.css └── toggle.css ├── 02_05b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── loader.css ├── script.js ├── style.css └── toggle.css ├── 02_05e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── loader.css ├── script.js ├── style.css └── toggle.css ├── 02_06b ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── loader.css ├── script.js ├── style.css └── toggle.css ├── 02_06e ├── components │ ├── Card.js │ ├── Cardlist.js │ └── cardlist.css ├── data.js ├── index.html ├── loader.css ├── script.js ├── style.css └── toggle.css ├── 03_02b ├── index.html └── script.js ├── 03_02e ├── index.html └── script.js ├── 03_03b ├── components │ └── weathercard.js ├── index.html ├── loader.css ├── script.js └── style.css ├── 03_03e ├── components │ └── weathercard.js ├── index.html ├── loader.css ├── script.js └── style.css ├── 03_04b ├── components │ └── weathercard.js ├── index.html ├── loader.css ├── script.js └── style.css ├── 03_04e ├── components │ └── weathercard.js ├── index.html ├── loader.css ├── script.js └── style.css ├── 03_05b ├── components │ └── weathercard.js ├── index.html ├── loader.css ├── script.js └── style.css ├── 03_05e ├── components │ └── weathercard.js ├── index.html ├── loader.css ├── script.js └── style.css ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── favicon.ico ├── package-lock.json ├── package.json └── settings.js /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | "GitHub.github-vscode-theme", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "ritwickdey.LiveServer", 7 | "stylelint.vscode-stylelint", 8 | "rangav.vscode-thunder-client" 9 | // Additional Extensions Here 10 | ], 11 | "onCreateCommand": "echo PS1='\"$ \"' >> ~/.bashrc" //Set Terminal Prompt to $ 12 | } 13 | 14 | // DevContainer Reference: https://code.visualstudio.com/docs/remote/devcontainerjson-reference 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["standard", "prettier"], 7 | parserOptions: { 8 | ecmaVersion: "latest", 9 | sourceType: "module", 10 | }, 11 | rules: {}, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners for these exercise files: 2 | # * (asterisk) denotes "all files and folders" 3 | # Example: * @producer @instructor 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Issue Overview 9 | 10 | 11 | ## Describe your environment 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Possible Solution 28 | 29 | 30 | ## Screenshots / Video 31 | 32 | 33 | ## Related Issues 34 | 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Copy To Branches 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | copy-to-branches: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Copy To Branches Action 12 | uses: planetoftheweb/copy-to-branches@v1 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.cursorBlinking": "solid", 4 | "editor.fontFamily": "ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace", 5 | "editor.fontLigatures": false, 6 | "editor.fontSize": 22, 7 | "editor.formatOnPaste": true, 8 | "editor.formatOnSave": true, 9 | "editor.lineNumbers": "on", 10 | "editor.matchBrackets": "always", 11 | "editor.minimap.enabled": false, 12 | "editor.smoothScrolling": true, 13 | "editor.tabSize": 2, 14 | "editor.useTabStops": true, 15 | "emmet.triggerExpansionOnTab": true, 16 | "explorer.openEditors.visible": 0, 17 | "files.autoSave": "afterDelay", 18 | "screencastMode.onlyKeyboardShortcuts": true, 19 | "terminal.integrated.fontSize": 18, 20 | "workbench.activityBar.visible": true, 21 | "workbench.colorTheme": "Visual Studio Dark", 22 | "workbench.fontAliasing": "antialiased", 23 | "workbench.statusBar.visible": true, 24 | "liveServer.settings.root": "/docs", 25 | "prettier.enable": true, 26 | "eslint.alwaysShowStatus": false, 27 | "liveServer.settings.donotVerifyTags": true, 28 | "editor.defaultFormatter": "esbenp.prettier-vscode" 29 | } 30 | -------------------------------------------------------------------------------- /01_01b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Data-driven image card 10 | 11 | 12 |
13 |

Data-driven image card

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /01_01b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Add date to output. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date 5 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString 6 | */ 7 | 8 | import data from "./data.js"; 9 | 10 | const mainContent = document.querySelector(".main-content"); 11 | 12 | const Card = (data) => { 13 | const imgData = data[0]; 14 | 15 | const markup = ` 16 |
17 | ${imgData.description} 30 |
31 |

${imgData.description}

32 |
33 |

34 | Photo by 35 | ${imgData.user.name}. 36 |

37 |

38 | 39 | View it on Unsplash. 40 | 41 |

42 |
43 |
44 |
45 | `; 46 | 47 | mainContent.innerHTML = markup; 48 | }; 49 | 50 | Card(data); 51 | -------------------------------------------------------------------------------- /01_01b/style.css: -------------------------------------------------------------------------------- 1 | img { 2 | display: block; 3 | width: 100%; 4 | height: auto; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | align-items: center; 10 | flex-direction: column; 11 | } 12 | 13 | .image { 14 | border: 3px solid black; 15 | } 16 | 17 | .image__caption { 18 | padding: 1rem 2rem; 19 | } 20 | 21 | .main-content { 22 | width: min(90vw, 80ch); 23 | } 24 | -------------------------------------------------------------------------------- /01_01e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Data-driven image card 10 | 11 | 12 |
13 |

Data-driven image card

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /01_01e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Add date to output. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date 5 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString 6 | */ 7 | 8 | import data from "./data.js"; 9 | 10 | const mainContent = document.querySelector(".main-content"); 11 | 12 | const Card = (data) => { 13 | const imgData = data[0]; 14 | const date = new Date(imgData.created_at); 15 | 16 | const markup = ` 17 |
18 | ${imgData.description} 31 |
32 |

${imgData.description}

33 |
34 |

35 | Photo by 36 | ${imgData.user.name}. 37 |

38 |

39 | Uploaded on 40 | . 47 |

48 |

49 | 50 | View it on Unsplash. 51 | 52 |

53 |
54 |
55 |
56 | `; 57 | 58 | mainContent.innerHTML = markup; 59 | }; 60 | 61 | Card(data); 62 | -------------------------------------------------------------------------------- /01_01e/style.css: -------------------------------------------------------------------------------- 1 | img { 2 | display: block; 3 | width: 100%; 4 | height: auto; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | align-items: center; 10 | flex-direction: column; 11 | } 12 | 13 | .image { 14 | border: 3px solid black; 15 | } 16 | 17 | .image__caption { 18 | padding: 1rem 2rem; 19 | } 20 | 21 | .main-content { 22 | width: min(90vw, 80ch); 23 | } 24 | -------------------------------------------------------------------------------- /01_02b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Break down a complex function 10 | 11 | 12 |
13 |

Break down a complex function

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /01_02b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Break down a complex function: 3 | * - Create a new function to build the element. 4 | * - Make srcset entries conditional on data being available. 5 | */ 6 | 7 | import data from "./data.js"; 8 | 9 | const mainContent = document.querySelector(".main-content"); 10 | 11 | const Card = (data) => { 12 | const imgData = data[0]; 13 | const date = new Date(imgData.created_at); 14 | 15 | const markup = ` 16 |
17 | ${imgData.description} 30 |
31 |

${imgData.description}

32 |
33 |

34 | Photo by 35 | ${imgData.user.name}. 36 |

37 |

38 | Uploaded on 39 | . 46 |

47 |

48 | 49 | View it on Unsplash. 50 | 51 |

52 |
53 |
54 |
55 | `; 56 | 57 | mainContent.innerHTML = markup; 58 | }; 59 | 60 | Card(data); 61 | -------------------------------------------------------------------------------- /01_02b/style.css: -------------------------------------------------------------------------------- 1 | img { 2 | display: block; 3 | width: 100%; 4 | height: auto; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | align-items: center; 10 | flex-direction: column; 11 | } 12 | 13 | .image { 14 | border: 3px solid black; 15 | } 16 | 17 | .image__caption { 18 | padding: 1rem 2rem; 19 | } 20 | 21 | .main-content { 22 | width: min(90vw, 80ch); 23 | } 24 | -------------------------------------------------------------------------------- /01_02e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Break down a complex function 10 | 11 | 12 |
13 |

Break down a complex function

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /01_02e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Break down a complex function: 3 | * - Create a new function to build the element. 4 | * - Make srcset entries conditional on data being available. 5 | */ 6 | 7 | import data from "./data.js"; 8 | 9 | const mainContent = document.querySelector(".main-content"); 10 | 11 | const buildImage = (imgData) => { 12 | let srcset = `${imgData.urls.full} ${imgData.width}w`; 13 | if (imgData.urls.regular) { 14 | srcset = srcset + `, ${imgData.urls.regular} 1080w`; 15 | } 16 | if (imgData.urls.small) { 17 | srcset = srcset + `, ${imgData.urls.small} 400w`; 18 | } 19 | 20 | const img = ` 21 | ${imgData.description} 30 | `; 31 | return img; 32 | }; 33 | 34 | const getDate = (imgData) => { 35 | const date = new Date(imgData.created_at); 36 | const niceDate = date.toLocaleString("default", { 37 | year: "numeric", 38 | month: "long", 39 | day: "numeric", 40 | }); 41 | return niceDate; 42 | }; 43 | 44 | const Card = (data) => { 45 | const imgData = data[0]; 46 | 47 | const markup = ` 48 |
49 | ${buildImage(imgData)} 50 |
51 |

${imgData.description}

52 |
53 |

54 | Photo by 55 | ${imgData.user.name}. 56 |

57 |

58 | Uploaded on . 61 |

62 |

63 | 64 | View it on Unsplash. 65 | 66 |

67 |
68 |
69 |
70 | `; 71 | 72 | mainContent.innerHTML = markup; 73 | }; 74 | 75 | Card(data); 76 | -------------------------------------------------------------------------------- /01_02e/style.css: -------------------------------------------------------------------------------- 1 | img { 2 | display: block; 3 | width: 100%; 4 | height: auto; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | align-items: center; 10 | flex-direction: column; 11 | } 12 | 13 | .image { 14 | border: 3px solid black; 15 | } 16 | 17 | .image__caption { 18 | padding: 1rem 2rem; 19 | } 20 | 21 | .main-content { 22 | width: min(90vw, 80ch); 23 | } 24 | -------------------------------------------------------------------------------- /01_03b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = (imgData) => { 2 | let srcset = `${imgData.urls.full} ${imgData.width}w`; 3 | if (imgData.urls.regular) { 4 | srcset = srcset + `, ${imgData.urls.regular} 1080w`; 5 | } 6 | if (imgData.urls.small) { 7 | srcset = srcset + `, ${imgData.urls.small} 400w`; 8 | } 9 | 10 | const img = ` 11 | ${imgData.description} 20 | `; 21 | return img; 22 | }; 23 | 24 | const getDate = (imgData) => { 25 | const date = new Date(imgData.created_at); 26 | const niceDate = date.toLocaleString("default", { 27 | year: "numeric", 28 | month: "long", 29 | day: "numeric", 30 | }); 31 | return niceDate; 32 | }; 33 | 34 | const Card = (data) => { 35 | const imgData = data[0]; 36 | 37 | return ` 38 |
39 | ${buildImage(imgData)} 40 |
41 |

${imgData.description}

42 |
43 |

44 | Photo by 45 | ${imgData.user.name}. 46 |

47 |

48 | Uploaded on . 51 |

52 |

53 | 54 | View it on Unsplash. 55 | 56 |

57 |
58 |
59 |
60 | `; 61 | }; 62 | 63 | export default Card; 64 | -------------------------------------------------------------------------------- /01_03b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid black; 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid black; 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /01_03b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Nesting components 10 | 11 | 12 |
13 |

Nesting components

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /01_03b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Nesting components: 3 | * - Create a new cardlist component. 4 | * - Iterate through all available images in `data.js`. 5 | * - Output a card for each available image. 6 | */ 7 | 8 | import data from "./data.js"; 9 | import Card from "./components/Card.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | 13 | mainContent.innerHTML = Card(data); 14 | -------------------------------------------------------------------------------- /01_03b/style.css: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0; 3 | } 4 | 5 | img { 6 | display: block; 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | } 16 | 17 | .image { 18 | border: 3px solid black; 19 | } 20 | 21 | .image__caption { 22 | padding: 1rem 2rem; 23 | } 24 | 25 | .main-content { 26 | width: min(90vw, 80ch); 27 | } 28 | -------------------------------------------------------------------------------- /01_03e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = (imgData) => { 2 | let srcset = `${imgData.urls.full} ${imgData.width}w`; 3 | if (imgData.urls.regular) { 4 | srcset = srcset + `, ${imgData.urls.regular} 1080w`; 5 | } 6 | if (imgData.urls.small) { 7 | srcset = srcset + `, ${imgData.urls.small} 400w`; 8 | } 9 | 10 | const img = ` 11 | ${imgData.description} 20 | `; 21 | return img; 22 | }; 23 | 24 | const getDate = (imgData) => { 25 | const date = new Date(imgData.created_at); 26 | const niceDate = date.toLocaleString("default", { 27 | year: "numeric", 28 | month: "long", 29 | day: "numeric", 30 | }); 31 | return niceDate; 32 | }; 33 | 34 | const Card = (imgData) => { 35 | return ` 36 |
37 | ${buildImage(imgData)} 38 |
39 |

${imgData.description}

40 |
41 |

42 | Photo by 43 | ${imgData.user.name}. 44 |

45 |

46 | Uploaded on . 49 |

50 |

51 | 52 | View it on Unsplash. 53 | 54 |

55 |
56 |
57 |
58 | `; 59 | }; 60 | 61 | export default Card; 62 | -------------------------------------------------------------------------------- /01_03e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /01_03e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid black; 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid black; 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /01_03e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Nesting components 10 | 11 | 12 |
    13 |

    Nesting components

    14 |
    15 |
    16 | 17 | 18 | -------------------------------------------------------------------------------- /01_03e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Nesting components: 3 | * - Create a new cardlist component. 4 | * - Iterate through all available images in `data.js`. 5 | * - Output a card for each available image. 6 | */ 7 | 8 | import data from "./data.js"; 9 | import Cardlist from "./components/Cardlist.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | 13 | mainContent.innerHTML = Cardlist(data); 14 | -------------------------------------------------------------------------------- /01_03e/style.css: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0; 3 | } 4 | 5 | img { 6 | display: block; 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | } 16 | 17 | .main-content { 18 | width: min(90vw, 120ch); 19 | } 20 | -------------------------------------------------------------------------------- /01_04b/components/Card.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use object destructuring to pick relevant properties from the data object. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 5 | */ 6 | 7 | const buildImage = (imgData) => { 8 | let srcset = `${imgData.urls.full} ${imgData.width}w`; 9 | if (imgData.urls.regular) { 10 | srcset = srcset + `, ${imgData.urls.regular} 1080w`; 11 | } 12 | if (imgData.urls.small) { 13 | srcset = srcset + `, ${imgData.urls.small} 400w`; 14 | } 15 | 16 | const img = ` 17 | ${imgData.description} 26 | `; 27 | return img; 28 | }; 29 | 30 | const getDate = (imgData) => { 31 | const date = new Date(imgData.created_at); 32 | const niceDate = date.toLocaleString("default", { 33 | year: "numeric", 34 | month: "long", 35 | day: "numeric", 36 | }); 37 | return niceDate; 38 | }; 39 | 40 | const Card = (imgData) => { 41 | return ` 42 |
    43 | ${buildImage(imgData)} 44 |
    45 |

    ${imgData.description}

    46 |
    47 |

    48 | Photo by 49 | ${imgData.user.name}. 50 |

    51 |

    52 | Uploaded on . 55 |

    56 |

    57 | 58 | View it on Unsplash. 59 | 60 |

    61 |
    62 |
    63 |
    64 | `; 65 | }; 66 | 67 | export default Card; 68 | -------------------------------------------------------------------------------- /01_04b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /01_04b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid black; 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid black; 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /01_04b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Pull only the data you need from an object 10 | 11 | 12 |
    13 |

    Pull only the data you need from an object

    14 |
    15 |
    16 | 17 | 18 | -------------------------------------------------------------------------------- /01_04b/script.js: -------------------------------------------------------------------------------- 1 | import data from "./data.js"; 2 | import Cardlist from "./components/Cardlist.js"; 3 | 4 | const mainContent = document.querySelector(".main-content"); 5 | 6 | mainContent.innerHTML = Cardlist(data); 7 | -------------------------------------------------------------------------------- /01_04b/style.css: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0; 3 | } 4 | 5 | img { 6 | display: block; 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | } 16 | 17 | .main-content { 18 | width: min(90vw, 120ch); 19 | } 20 | -------------------------------------------------------------------------------- /01_04e/components/Card.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use object destructuring to pick relevant properties from the data object. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 5 | */ 6 | 7 | const buildImage = ({ 8 | urls: { full, regular, small }, 9 | width, 10 | height, 11 | description, 12 | }) => { 13 | let srcset = `${full} ${width}w`; 14 | if (regular) { 15 | srcset = srcset + `, ${regular} 1080w`; 16 | } 17 | if (small) { 18 | srcset = srcset + `, ${small} 400w`; 19 | } 20 | 21 | const img = ` 22 | ${description} 31 | `; 32 | return img; 33 | }; 34 | 35 | const getDate = (createdDate) => { 36 | const date = new Date(createdDate); 37 | const niceDate = date.toLocaleString("default", { 38 | year: "numeric", 39 | month: "long", 40 | day: "numeric", 41 | }); 42 | return niceDate; 43 | }; 44 | 45 | const Card = (imgData) => { 46 | const { 47 | description, 48 | user: { name }, 49 | created_at: createdDate, 50 | links: { self }, 51 | } = imgData; 52 | return ` 53 |
    54 | ${buildImage(imgData)} 55 |
    56 |

    ${description}

    57 |
    58 |

    59 | Photo by 60 | ${name}. 61 |

    62 |

    63 | Uploaded on . 66 |

    67 |

    68 | 69 | View it on Unsplash. 70 | 71 |

    72 |
    73 |
    74 |
    75 | `; 76 | }; 77 | 78 | export default Card; 79 | -------------------------------------------------------------------------------- /01_04e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /01_04e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid black; 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid black; 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /01_04e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Pull only the data you need from an object 10 | 11 | 12 |
    13 |

    Pull only the data you need from an object

    14 |
    15 |
    16 | 17 | 18 | -------------------------------------------------------------------------------- /01_04e/script.js: -------------------------------------------------------------------------------- 1 | import data from "./data.js"; 2 | import Cardlist from "./components/Cardlist.js"; 3 | 4 | const mainContent = document.querySelector(".main-content"); 5 | 6 | mainContent.innerHTML = Cardlist(data); 7 | -------------------------------------------------------------------------------- /01_04e/style.css: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0; 3 | } 4 | 5 | img { 6 | display: block; 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | } 16 | 17 | .main-content { 18 | width: min(90vw, 120ch); 19 | } 20 | -------------------------------------------------------------------------------- /01_05b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | } = imgData; 46 | return ` 47 |
    48 | ${buildImage(imgData)} 49 |
    50 |

    ${description}

    51 |
    52 |

    53 | Photo by 54 | ${name}. 55 |

    56 |

    57 | Uploaded on . 60 |

    61 |

    62 | 63 | View it on Unsplash. 64 | 65 |

    66 |
    67 |
    68 |
    69 | `; 70 | }; 71 | 72 | export default Card; 73 | -------------------------------------------------------------------------------- /01_05b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /01_05b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid black; 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid black; 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /01_05b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Add new data to a data object 10 | 11 | 12 |
    13 |

    Add new data to a data object

    14 |
    15 |
    16 | 17 | 18 | -------------------------------------------------------------------------------- /01_05b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use spread syntax to add new data to a data object. 3 | * - The license name is called "Unsplash License". 4 | * - The URL to the license is https://unsplash.com/license 5 | * References: 6 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax 7 | */ 8 | 9 | import data from "./data.js"; 10 | import Cardlist from "./components/Cardlist.js"; 11 | 12 | const mainContent = document.querySelector(".main-content"); 13 | 14 | mainContent.innerHTML = Cardlist(data); 15 | -------------------------------------------------------------------------------- /01_05b/style.css: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0; 3 | } 4 | 5 | img { 6 | display: block; 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | } 16 | 17 | .main-content { 18 | width: min(90vw, 120ch); 19 | } 20 | -------------------------------------------------------------------------------- /01_05e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /01_05e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /01_05e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid black; 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid black; 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /01_05e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Add new data to a data object 10 | 11 | 12 |
    13 |

    Add new data to a data object

    14 |
    15 |
    16 | 17 | 18 | -------------------------------------------------------------------------------- /01_05e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use spread syntax to add new data to a data object. 3 | * - The license name is called "Unsplash License". 4 | * - The URL to the license is https://unsplash.com/license 5 | * References: 6 | * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax 7 | */ 8 | 9 | import data from "./data.js"; 10 | import Cardlist from "./components/Cardlist.js"; 11 | 12 | // Add license info to each data object. 13 | const license = { 14 | license: "Unsplash License", 15 | license_uri: "https://unsplash.com/license", 16 | }; 17 | const newData = data.map((imgData) => { 18 | const newImgData = { ...imgData, ...license }; 19 | return newImgData; 20 | }); 21 | 22 | const mainContent = document.querySelector(".main-content"); 23 | 24 | mainContent.innerHTML = Cardlist(newData); 25 | -------------------------------------------------------------------------------- /01_05e/style.css: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0; 3 | } 4 | 5 | img { 6 | display: block; 7 | width: 100%; 8 | height: auto; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | } 16 | 17 | .main-content { 18 | width: min(90vw, 120ch); 19 | } 20 | -------------------------------------------------------------------------------- /02_02b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_02b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_02b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_02b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Dark Mode Toggle 10 | 11 | 12 |
    13 |

    Dark Mode Toggle

    14 | 15 | 16 | 17 | 91 | 92 |
    93 |
    94 | 95 | 96 | -------------------------------------------------------------------------------- /02_02b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a light/dark mode switch. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme 5 | * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList 6 | */ 7 | 8 | import data from "./data.js"; 9 | import Cardlist from "./components/Cardlist.js"; 10 | 11 | // Add license info to each data object. 12 | const license = { 13 | license: "Unsplash License", 14 | license_uri: "https://unsplash.com/license", 15 | }; 16 | const newData = data.map((imgData) => { 17 | const newImgData = { ...imgData, ...license }; 18 | return newImgData; 19 | }); 20 | 21 | const mainContent = document.querySelector(".main-content"); 22 | 23 | mainContent.innerHTML = Cardlist(newData); 24 | 25 | /** 26 | * Light/dark mode feature. 27 | */ 28 | const toggle = document.querySelector(".toggle"); 29 | 30 | // Trigger mode change with toggle. 31 | const toggleDisplayMode = () => { 32 | if (toggle.getAttribute("aria-pressed") === "true") { 33 | toggle.removeAttribute("aria-pressed"); 34 | } else { 35 | toggle.setAttribute("aria-pressed", "true"); 36 | } 37 | }; 38 | toggle.addEventListener("click", () => toggleDisplayMode()); 39 | -------------------------------------------------------------------------------- /02_02b/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | -------------------------------------------------------------------------------- /02_02b/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_02e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_02e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_02e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_02e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a light/dark mode switch. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme 5 | * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList 6 | */ 7 | 8 | import data from "./data.js"; 9 | import Cardlist from "./components/Cardlist.js"; 10 | 11 | // Add license info to each data object. 12 | const license = { 13 | license: "Unsplash License", 14 | license_uri: "https://unsplash.com/license", 15 | }; 16 | const newData = data.map((imgData) => { 17 | const newImgData = { ...imgData, ...license }; 18 | return newImgData; 19 | }); 20 | 21 | const mainContent = document.querySelector(".main-content"); 22 | 23 | mainContent.innerHTML = Cardlist(newData); 24 | 25 | /** 26 | * Light/dark mode feature. 27 | */ 28 | const toggle = document.querySelector(".toggle"); 29 | const docElement = document.documentElement; 30 | 31 | // Trigger mode change with toggle. 32 | const toggleDisplayMode = () => { 33 | if (toggle.getAttribute("aria-pressed") === "true") { 34 | toggle.removeAttribute("aria-pressed"); 35 | } else { 36 | toggle.setAttribute("aria-pressed", "true"); 37 | } 38 | 39 | docElement.classList.toggle("dark"); 40 | docElement.classList.toggle("light"); 41 | }; 42 | toggle.addEventListener("click", () => toggleDisplayMode()); 43 | -------------------------------------------------------------------------------- /02_02e/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | -------------------------------------------------------------------------------- /02_02e/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_03b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_03b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_03b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_03b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Dark Mode Toggle 10 | 11 | 12 |
    13 |

    Dark Mode Toggle

    14 | 15 | 16 | 17 | 91 | 92 |
    93 |
    94 | 95 | 96 | -------------------------------------------------------------------------------- /02_03b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Detect browser color scheme. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia 5 | * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList 6 | */ 7 | 8 | import data from "./data.js"; 9 | import Cardlist from "./components/Cardlist.js"; 10 | 11 | // Add license info to each data object. 12 | const license = { 13 | license: "Unsplash License", 14 | license_uri: "https://unsplash.com/license", 15 | }; 16 | const newData = data.map((imgData) => { 17 | const newImgData = { ...imgData, ...license }; 18 | return newImgData; 19 | }); 20 | 21 | const mainContent = document.querySelector(".main-content"); 22 | 23 | mainContent.innerHTML = Cardlist(newData); 24 | 25 | /** 26 | * Light/dark mode feature. 27 | */ 28 | const toggle = document.querySelector(".toggle"); 29 | const docElement = document.documentElement; 30 | 31 | // Trigger mode change with toggle. 32 | const toggleDisplayMode = () => { 33 | if (toggle.getAttribute("aria-pressed") === "true") { 34 | toggle.removeAttribute("aria-pressed"); 35 | } else { 36 | toggle.setAttribute("aria-pressed", "true"); 37 | } 38 | 39 | docElement.classList.toggle("light"); 40 | docElement.classList.toggle("dark"); 41 | }; 42 | toggle.addEventListener("click", () => toggleDisplayMode()); 43 | -------------------------------------------------------------------------------- /02_03b/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | -------------------------------------------------------------------------------- /02_03b/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_03e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_03e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_03e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_03e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a light/dark mode switch. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia 5 | * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList 6 | */ 7 | 8 | import data from "./data.js"; 9 | import Cardlist from "./components/Cardlist.js"; 10 | 11 | // Add license info to each data object. 12 | const license = { 13 | license: "Unsplash License", 14 | license_uri: "https://unsplash.com/license", 15 | }; 16 | const newData = data.map((imgData) => { 17 | const newImgData = { ...imgData, ...license }; 18 | return newImgData; 19 | }); 20 | 21 | const mainContent = document.querySelector(".main-content"); 22 | 23 | mainContent.innerHTML = Cardlist(newData); 24 | 25 | /** 26 | * Light/dark mode feature. 27 | */ 28 | const docElement = document.documentElement; 29 | const toggle = document.querySelector(".toggle"); 30 | 31 | // Detect mode on load and set toggle state accordingly. 32 | const displayModeOnLoad = () => { 33 | if ( 34 | window.matchMedia && 35 | window.matchMedia("(prefers-color-scheme: dark)").matches 36 | ) { 37 | docElement.classList.add("dark"); 38 | toggle.setAttribute("aria-pressed", "true"); 39 | } else { 40 | docElement.classList.add("light"); 41 | toggle.removeAttribute("aria-pressed"); 42 | } 43 | }; 44 | displayModeOnLoad(); 45 | 46 | // Trigger mode change with toggle. 47 | const toggleDisplayMode = () => { 48 | if (toggle.getAttribute("aria-pressed") === "true") { 49 | toggle.removeAttribute("aria-pressed"); 50 | } else { 51 | toggle.setAttribute("aria-pressed", "true"); 52 | } 53 | 54 | docElement.classList.toggle("dark"); 55 | docElement.classList.toggle("light"); 56 | }; 57 | toggle.addEventListener("click", () => toggleDisplayMode()); 58 | -------------------------------------------------------------------------------- /02_03e/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | -------------------------------------------------------------------------------- /02_03e/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_04b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_04b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_04b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_04b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Store dark mode user preference in localstorage. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage 5 | */ 6 | 7 | import data from "./data.js"; 8 | import Cardlist from "./components/Cardlist.js"; 9 | 10 | // Add license info to each data object. 11 | const license = { 12 | license: "Unsplash License", 13 | license_uri: "https://unsplash.com/license", 14 | }; 15 | const newData = data.map((imgData) => { 16 | const newImgData = { ...imgData, ...license }; 17 | return newImgData; 18 | }); 19 | 20 | const mainContent = document.querySelector(".main-content"); 21 | 22 | mainContent.innerHTML = Cardlist(newData); 23 | 24 | /** 25 | * Light/dark mode feature. 26 | */ 27 | const docElement = document.documentElement; 28 | const toggle = document.querySelector(".toggle"); 29 | 30 | // Detect mode on load and set toggle state accordingly. 31 | const displayModeOnLoad = () => { 32 | if ( 33 | window.matchMedia && 34 | window.matchMedia("(prefers-color-scheme: dark)").matches 35 | ) { 36 | docElement.classList.add("dark"); 37 | toggle.setAttribute("aria-pressed", "true"); 38 | } else { 39 | docElement.classList.add("light"); 40 | toggle.removeAttribute("aria-pressed"); 41 | } 42 | }; 43 | displayModeOnLoad(); 44 | 45 | // Trigger mode change with toggle. 46 | const toggleDisplayMode = () => { 47 | if (toggle.getAttribute("aria-pressed") === "true") { 48 | toggle.removeAttribute("aria-pressed"); 49 | } else { 50 | toggle.setAttribute("aria-pressed", "true"); 51 | } 52 | 53 | docElement.classList.toggle("dark"); 54 | docElement.classList.toggle("light"); 55 | }; 56 | toggle.addEventListener("click", () => toggleDisplayMode()); 57 | -------------------------------------------------------------------------------- /02_04b/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | -------------------------------------------------------------------------------- /02_04b/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_04e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_04e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_04e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_04e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Store dark mode user preference in localstorage. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage 5 | */ 6 | 7 | import data from "./data.js"; 8 | import Cardlist from "./components/Cardlist.js"; 9 | 10 | // Add license info to each data object. 11 | const license = { 12 | license: "Unsplash License", 13 | license_uri: "https://unsplash.com/license", 14 | }; 15 | const newData = data.map((imgData) => { 16 | const newImgData = { ...imgData, ...license }; 17 | return newImgData; 18 | }); 19 | 20 | const mainContent = document.querySelector(".main-content"); 21 | 22 | mainContent.innerHTML = Cardlist(newData); 23 | 24 | /** 25 | * Light/dark mode feature. 26 | */ 27 | const docElement = document.documentElement; 28 | const toggle = document.querySelector(".toggle"); 29 | 30 | // Detect mode on load and set toggle state accordingly. 31 | const displayModeOnLoad = () => { 32 | console.log(localStorage.getItem("darkMode")); 33 | let dark = false; 34 | // Set dark to true if prefers-color-scheme is set to dark. 35 | dark = !!( 36 | window.matchMedia && 37 | window.matchMedia("(prefers-color-scheme: dark)").matches 38 | ); 39 | console.log(dark); 40 | // Set dark to true of localStorage "darkMode" is set to enabled. 41 | dark = localStorage.getItem("darkMode") === "enabled"; 42 | console.log(dark); 43 | 44 | if (dark) { 45 | docElement.classList.add("dark"); 46 | toggle.setAttribute("aria-pressed", "true"); 47 | localStorage.setItem("darkMode", "enabled"); 48 | } else { 49 | docElement.classList.add("light"); 50 | toggle.removeAttribute("aria-pressed"); 51 | localStorage.setItem("darkMode", "disabled"); 52 | } 53 | }; 54 | displayModeOnLoad(); 55 | 56 | // Trigger mode change with toggle. 57 | const toggleDisplayMode = () => { 58 | if (toggle.getAttribute("aria-pressed") === "true") { 59 | toggle.removeAttribute("aria-pressed"); 60 | localStorage.setItem("darkMode", "disabled"); 61 | } else { 62 | toggle.setAttribute("aria-pressed", "true"); 63 | localStorage.setItem("darkMode", "enabled"); 64 | } 65 | 66 | docElement.classList.toggle("dark"); 67 | docElement.classList.toggle("light"); 68 | }; 69 | toggle.addEventListener("click", () => toggleDisplayMode()); 70 | -------------------------------------------------------------------------------- /02_04e/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | -------------------------------------------------------------------------------- /02_04e/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_05b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_05b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_05b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_05b/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02_05b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a "loading" indicator triggered on button click. 3 | * - Use CSS to hide/show the loader. 4 | * - Create artificial delay of loading using setTimeout() 5 | * References: 6 | * - https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 7 | * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList 8 | * - https://developer.mozilla.org/en-US/docs/Web/API/setTimeout 9 | */ 10 | 11 | import data from "./data.js"; 12 | import Cardlist from "./components/Cardlist.js"; 13 | 14 | // Add license info to each data object. 15 | const license = { 16 | license: "Unsplash License", 17 | license_uri: "https://unsplash.com/license", 18 | }; 19 | const newData = data.map((imgData) => { 20 | const newImgData = { ...imgData, ...license }; 21 | return newImgData; 22 | }); 23 | 24 | const mainContent = document.querySelector(".main-content"); 25 | 26 | mainContent.innerHTML = Cardlist(newData); 27 | 28 | /** 29 | * Light/dark mode feature. 30 | */ 31 | const docElement = document.documentElement; 32 | const toggle = document.querySelector(".toggle"); 33 | 34 | // Detect mode on load and set toggle state accordingly. 35 | const displayModeOnLoad = () => { 36 | console.log(localStorage.getItem("darkMode")); 37 | let dark = false; 38 | // Set dark to true if prefers-color-scheme is set to dark. 39 | dark = !!( 40 | window.matchMedia && 41 | window.matchMedia("(prefers-color-scheme: dark)").matches 42 | ); 43 | console.log(dark); 44 | // Set dark to true of localStorage "darkMode" is set to enabled. 45 | dark = localStorage.getItem("darkMode") === "enabled"; 46 | console.log(dark); 47 | 48 | if (dark) { 49 | docElement.classList.add("dark"); 50 | toggle.setAttribute("aria-pressed", "true"); 51 | localStorage.setItem("darkMode", "enabled"); 52 | } else { 53 | docElement.classList.add("light"); 54 | toggle.removeAttribute("aria-pressed"); 55 | localStorage.setItem("darkMode", "disabled"); 56 | } 57 | }; 58 | displayModeOnLoad(); 59 | 60 | // Trigger mode change with toggle. 61 | const toggleDisplayMode = () => { 62 | if (toggle.getAttribute("aria-pressed") === "true") { 63 | toggle.removeAttribute("aria-pressed"); 64 | localStorage.setItem("darkMode", "disabled"); 65 | } else { 66 | toggle.setAttribute("aria-pressed", "true"); 67 | localStorage.setItem("darkMode", "enabled"); 68 | } 69 | 70 | docElement.classList.toggle("dark"); 71 | docElement.classList.toggle("light"); 72 | }; 73 | toggle.addEventListener("click", () => toggleDisplayMode()); 74 | -------------------------------------------------------------------------------- /02_05b/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | 57 | /* Card styles */ 58 | .cardlist { 59 | border: 3px solid var(--contrast-color, black); 60 | padding: 1rem; 61 | } 62 | 63 | .cardlist__list { 64 | display: flex; 65 | gap: 1rem; 66 | margin: 0; 67 | padding: 0; 68 | list-style-type: none; 69 | } 70 | 71 | .image { 72 | border: 3px solid var(--contrast-color, black); 73 | } 74 | 75 | .image__caption { 76 | padding: 0.5rem 1rem; 77 | } 78 | 79 | /* Skeleton styles */ 80 | .skeleton { 81 | --skeleton-color: hsl(0, 0%, 75%); 82 | display: grid; 83 | } 84 | 85 | .skeleton .loader, 86 | .skeleton #load, 87 | .skeleton .cardlist__list { 88 | grid-column: 1/1; 89 | grid-row: 1/1; 90 | } 91 | 92 | .skeleton .loader, 93 | .skeleton #load { 94 | z-index: 1; 95 | justify-self: center; 96 | align-self: center; 97 | } 98 | 99 | .skeleton .cardlist__list { 100 | justify-content: space-between; 101 | opacity: 50%; 102 | } 103 | 104 | .skeleton .cardlist__item { 105 | flex-grow: 1; 106 | } 107 | 108 | .skeleton .image { 109 | border-color: var(--skeleton-color); 110 | } 111 | .skeleton .image > div { 112 | width: 100%; 113 | aspect-ratio: 4/5; 114 | background: var(--skeleton-color); 115 | } 116 | 117 | .skeleton h3, 118 | .skeleton p { 119 | content: ""; 120 | width: 100%; 121 | height: 1rem; 122 | background: var(--skeleton-color); 123 | } 124 | 125 | .hidden { 126 | display: none; 127 | } 128 | -------------------------------------------------------------------------------- /02_05b/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_05e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_05e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_05e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_05e/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02_05e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a "loading" indicator triggered on button click. 3 | * - Use CSS to hide/show the loader. 4 | * - Create artificial delay of loading using setTimeout() 5 | * References: 6 | * - https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 7 | * - https://developer.mozilla.org/en-US/docs/Web/API/Element/classList 8 | * - https://developer.mozilla.org/en-US/docs/Web/API/setTimeout 9 | */ 10 | 11 | import data from "./data.js"; 12 | import Cardlist from "./components/Cardlist.js"; 13 | 14 | // Add license info to each data object. 15 | const license = { 16 | license: "Unsplash License", 17 | license_uri: "https://unsplash.com/license", 18 | }; 19 | const newData = data.map((imgData) => { 20 | const newImgData = { ...imgData, ...license }; 21 | return newImgData; 22 | }); 23 | 24 | const mainContent = document.querySelector(".main-content"); 25 | const loadButton = document.querySelector("#load"); 26 | const loader = document.querySelector(".loader"); 27 | 28 | loadButton.addEventListener("click", () => { 29 | loader.classList.toggle("hidden"); 30 | loadButton.classList.toggle("hidden"); 31 | setTimeout(() => { 32 | mainContent.innerHTML = Cardlist(newData); 33 | }, 3000); 34 | }); 35 | 36 | /** 37 | * Light/dark mode feature. 38 | */ 39 | const docElement = document.documentElement; 40 | const toggle = document.querySelector(".toggle"); 41 | 42 | // Detect mode on load and set toggle state accordingly. 43 | const displayModeOnLoad = () => { 44 | console.log(localStorage.getItem("darkMode")); 45 | let dark = false; 46 | // Set dark to true if prefers-color-scheme is set to dark. 47 | dark = !!( 48 | window.matchMedia && 49 | window.matchMedia("(prefers-color-scheme: dark)").matches 50 | ); 51 | console.log(dark); 52 | // Set dark to true of localStorage "darkMode" is set to enabled. 53 | dark = localStorage.getItem("darkMode") === "enabled"; 54 | console.log(dark); 55 | 56 | if (dark) { 57 | docElement.classList.add("dark"); 58 | toggle.setAttribute("aria-pressed", "true"); 59 | localStorage.setItem("darkMode", "enabled"); 60 | } else { 61 | docElement.classList.add("light"); 62 | toggle.removeAttribute("aria-pressed"); 63 | localStorage.setItem("darkMode", "disabled"); 64 | } 65 | }; 66 | displayModeOnLoad(); 67 | 68 | // Trigger mode change with toggle. 69 | const toggleDisplayMode = () => { 70 | if (toggle.getAttribute("aria-pressed") === "true") { 71 | toggle.removeAttribute("aria-pressed"); 72 | localStorage.setItem("darkMode", "disabled"); 73 | } else { 74 | toggle.setAttribute("aria-pressed", "true"); 75 | localStorage.setItem("darkMode", "enabled"); 76 | } 77 | 78 | docElement.classList.toggle("dark"); 79 | docElement.classList.toggle("light"); 80 | }; 81 | toggle.addEventListener("click", () => toggleDisplayMode()); 82 | -------------------------------------------------------------------------------- /02_05e/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | } 56 | 57 | /* Card styles */ 58 | .cardlist { 59 | border: 3px solid var(--contrast-color, black); 60 | padding: 1rem; 61 | } 62 | 63 | .cardlist__list { 64 | display: flex; 65 | gap: 1rem; 66 | margin: 0; 67 | padding: 0; 68 | list-style-type: none; 69 | } 70 | 71 | .image { 72 | border: 3px solid var(--contrast-color, black); 73 | } 74 | 75 | .image__caption { 76 | padding: 0.5rem 1rem; 77 | } 78 | 79 | /* Skeleton styles */ 80 | .skeleton { 81 | --skeleton-color: hsl(0, 0%, 75%); 82 | display: grid; 83 | } 84 | 85 | .skeleton .loader, 86 | .skeleton #load, 87 | .skeleton .cardlist__list { 88 | grid-column: 1/1; 89 | grid-row: 1/1; 90 | } 91 | 92 | .skeleton .loader, 93 | .skeleton #load { 94 | z-index: 1; 95 | justify-self: center; 96 | align-self: center; 97 | } 98 | 99 | .skeleton .cardlist__list { 100 | justify-content: space-between; 101 | opacity: 50%; 102 | } 103 | 104 | .skeleton .cardlist__item { 105 | flex-grow: 1; 106 | } 107 | 108 | .skeleton .image { 109 | border-color: var(--skeleton-color); 110 | } 111 | .skeleton .image > div { 112 | width: 100%; 113 | aspect-ratio: 4/5; 114 | background: var(--skeleton-color); 115 | } 116 | 117 | .skeleton h3, 118 | .skeleton p { 119 | content: ""; 120 | width: 100%; 121 | height: 1rem; 122 | background: var(--skeleton-color); 123 | } 124 | 125 | .hidden { 126 | display: none; 127 | } 128 | -------------------------------------------------------------------------------- /02_05e/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_06b/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_06b/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_06b/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_06b/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02_06b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Laxy-Load content on scroll. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 5 | */ 6 | 7 | import data from "./data.js"; 8 | import Cardlist from "./components/Cardlist.js"; 9 | 10 | // Add license info to each data object. 11 | const license = { 12 | license: "Unsplash License", 13 | license_uri: "https://unsplash.com/license", 14 | }; 15 | const newData = data.map((imgData) => { 16 | const newImgData = { ...imgData, ...license }; 17 | return newImgData; 18 | }); 19 | 20 | const mainContent = document.querySelector(".main-content"); 21 | const loadButton = document.querySelector("#load"); 22 | const loader = document.querySelector(".loader"); 23 | 24 | loadButton.addEventListener("click", () => { 25 | loader.classList.toggle("hidden"); 26 | loadButton.classList.toggle("hidden"); 27 | setTimeout(() => { 28 | mainContent.innerHTML = Cardlist(newData); 29 | }, 3000); 30 | }); 31 | 32 | /** 33 | * Light/dark mode feature. 34 | */ 35 | const docElement = document.documentElement; 36 | const toggle = document.querySelector(".toggle"); 37 | 38 | // Detect mode on load and set toggle state accordingly. 39 | const displayModeOnLoad = () => { 40 | console.log(localStorage.getItem("darkMode")); 41 | let dark = false; 42 | // Set dark to true if prefers-color-scheme is set to dark. 43 | dark = !!( 44 | window.matchMedia && 45 | window.matchMedia("(prefers-color-scheme: dark)").matches 46 | ); 47 | console.log(dark); 48 | // Set dark to true of localStorage "darkMode" is set to enabled. 49 | dark = localStorage.getItem("darkMode") === "enabled"; 50 | console.log(dark); 51 | 52 | if (dark) { 53 | docElement.classList.add("dark"); 54 | toggle.setAttribute("aria-pressed", "true"); 55 | localStorage.setItem("darkMode", "enabled"); 56 | } else { 57 | docElement.classList.add("light"); 58 | toggle.removeAttribute("aria-pressed"); 59 | localStorage.setItem("darkMode", "disabled"); 60 | } 61 | }; 62 | displayModeOnLoad(); 63 | 64 | // Trigger mode change with toggle. 65 | const toggleDisplayMode = () => { 66 | if (toggle.getAttribute("aria-pressed") === "true") { 67 | toggle.removeAttribute("aria-pressed"); 68 | localStorage.setItem("darkMode", "disabled"); 69 | } else { 70 | toggle.setAttribute("aria-pressed", "true"); 71 | localStorage.setItem("darkMode", "enabled"); 72 | } 73 | 74 | docElement.classList.toggle("dark"); 75 | docElement.classList.toggle("light"); 76 | }; 77 | toggle.addEventListener("click", () => toggleDisplayMode()); 78 | -------------------------------------------------------------------------------- /02_06b/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | margin-block-end: 5rem; 56 | } 57 | 58 | .filler { 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: center; 62 | align-items: center; 63 | height: 100vh; 64 | font-size: 5rem; 65 | } 66 | 67 | /* Card styles */ 68 | .cardlist { 69 | border: 3px solid var(--contrast-color, black); 70 | padding: 1rem; 71 | } 72 | 73 | .cardlist__list { 74 | display: flex; 75 | gap: 1rem; 76 | margin: 0; 77 | padding: 0; 78 | list-style-type: none; 79 | } 80 | 81 | .image { 82 | border: 3px solid var(--contrast-color, black); 83 | } 84 | 85 | .image__caption { 86 | padding: 0.5rem 1rem; 87 | } 88 | 89 | /* Skeleton styles */ 90 | .skeleton { 91 | --skeleton-color: hsl(0, 0%, 75%); 92 | display: grid; 93 | } 94 | 95 | .skeleton .loader, 96 | .skeleton #load, 97 | .skeleton .cardlist__list { 98 | grid-column: 1/1; 99 | grid-row: 1/1; 100 | } 101 | 102 | .skeleton .loader, 103 | .skeleton #load { 104 | z-index: 1; 105 | justify-self: center; 106 | align-self: center; 107 | } 108 | 109 | .skeleton .cardlist__list { 110 | justify-content: space-between; 111 | opacity: 50%; 112 | } 113 | 114 | .skeleton .cardlist__item { 115 | flex-grow: 1; 116 | } 117 | 118 | .skeleton .image { 119 | border-color: var(--skeleton-color); 120 | } 121 | .skeleton .image > div { 122 | width: 100%; 123 | aspect-ratio: 4/5; 124 | background: var(--skeleton-color); 125 | } 126 | 127 | .skeleton h3, 128 | .skeleton p { 129 | content: ""; 130 | width: 100%; 131 | height: 1rem; 132 | background: var(--skeleton-color); 133 | } 134 | 135 | .hidden { 136 | display: none; 137 | } 138 | -------------------------------------------------------------------------------- /02_06b/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /02_06e/components/Card.js: -------------------------------------------------------------------------------- 1 | const buildImage = ({ 2 | urls: { full, regular, small }, 3 | width, 4 | height, 5 | description, 6 | }) => { 7 | let srcset = `${full} ${width}w`; 8 | if (regular) { 9 | srcset = srcset + `, ${regular} 1080w`; 10 | } 11 | if (small) { 12 | srcset = srcset + `, ${small} 400w`; 13 | } 14 | 15 | const img = ` 16 | ${description} 25 | `; 26 | return img; 27 | }; 28 | 29 | const getDate = (createdDate) => { 30 | const date = new Date(createdDate); 31 | const niceDate = date.toLocaleString("default", { 32 | year: "numeric", 33 | month: "long", 34 | day: "numeric", 35 | }); 36 | return niceDate; 37 | }; 38 | 39 | const Card = (imgData) => { 40 | const { 41 | description, 42 | user: { name }, 43 | created_at: createdDate, 44 | links: { self }, 45 | license, 46 | license_uri: licenseURL, 47 | } = imgData; 48 | return ` 49 |
    50 | ${buildImage(imgData)} 51 |
    52 |

    ${description}

    53 |
    54 |

    55 | Photo by 56 | ${name}. 57 |

    58 |

    59 | Uploaded on . 62 |

    63 |

    64 | 65 | View it on Unsplash. 66 | 67 |

    68 |

    License: ${license}.

    69 |
    70 |
    71 |
    72 | `; 73 | }; 74 | 75 | export default Card; 76 | -------------------------------------------------------------------------------- /02_06e/components/Cardlist.js: -------------------------------------------------------------------------------- 1 | import Card from "./Card.js"; 2 | 3 | const cardListItem = (imgData) => { 4 | return `
  • 5 | ${Card(imgData)} 6 |
  • `; 7 | }; 8 | 9 | const Cardlist = (data) => { 10 | return ` 11 | 12 |
    13 | 16 |
    17 | `; 18 | }; 19 | 20 | export default Cardlist; 21 | -------------------------------------------------------------------------------- /02_06e/components/cardlist.css: -------------------------------------------------------------------------------- 1 | .cardlist { 2 | border: 3px solid var(--contrast-color, black); 3 | padding: 1rem; 4 | } 5 | 6 | .cardlist__list { 7 | display: flex; 8 | gap: 1rem; 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | 14 | .image { 15 | border: 3px solid var(--contrast-color, black); 16 | } 17 | 18 | .image__caption { 19 | padding: 0.5rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /02_06e/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02_06e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Laxy-Load content on scroll. 3 | * References: 4 | * - https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 5 | */ 6 | 7 | import data from "./data.js"; 8 | import Cardlist from "./components/Cardlist.js"; 9 | 10 | // Add license info to each data object. 11 | const license = { 12 | license: "Unsplash License", 13 | license_uri: "https://unsplash.com/license", 14 | }; 15 | const newData = data.map((imgData) => { 16 | const newImgData = { ...imgData, ...license }; 17 | return newImgData; 18 | }); 19 | 20 | const mainContent = document.querySelector(".main-content"); 21 | const loadButton = document.querySelector("#load"); 22 | const loader = document.querySelector(".loader"); 23 | const target = document.querySelector(".cardlist"); 24 | 25 | const loadCards = (entries) => { 26 | console.log(entries); 27 | console.log("intersection"); 28 | entries.forEach(function (entry) { 29 | if (entry.isIntersecting) { 30 | loader.classList.toggle("hidden"); 31 | loadButton.classList.toggle("hidden"); 32 | setTimeout(() => { 33 | mainContent.innerHTML = Cardlist(newData); 34 | }, 3000); 35 | } 36 | }); 37 | }; 38 | 39 | // Intersection observer 40 | 41 | const observer = new IntersectionObserver(loadCards); 42 | 43 | observer.observe(target); 44 | 45 | /** 46 | * Light/dark mode feature. 47 | */ 48 | const docElement = document.documentElement; 49 | const toggle = document.querySelector(".toggle"); 50 | 51 | // Detect mode on load and set toggle state accordingly. 52 | const displayModeOnLoad = () => { 53 | console.log(localStorage.getItem("darkMode")); 54 | let dark = false; 55 | // Set dark to true if prefers-color-scheme is set to dark. 56 | dark = !!( 57 | window.matchMedia && 58 | window.matchMedia("(prefers-color-scheme: dark)").matches 59 | ); 60 | console.log(dark); 61 | // Set dark to true of localStorage "darkMode" is set to enabled. 62 | dark = localStorage.getItem("darkMode") === "enabled"; 63 | console.log(dark); 64 | 65 | if (dark) { 66 | docElement.classList.add("dark"); 67 | toggle.setAttribute("aria-pressed", "true"); 68 | localStorage.setItem("darkMode", "enabled"); 69 | } else { 70 | docElement.classList.add("light"); 71 | toggle.removeAttribute("aria-pressed"); 72 | localStorage.setItem("darkMode", "disabled"); 73 | } 74 | }; 75 | displayModeOnLoad(); 76 | 77 | // Trigger mode change with toggle. 78 | const toggleDisplayMode = () => { 79 | if (toggle.getAttribute("aria-pressed") === "true") { 80 | toggle.removeAttribute("aria-pressed"); 81 | localStorage.setItem("darkMode", "disabled"); 82 | } else { 83 | toggle.setAttribute("aria-pressed", "true"); 84 | localStorage.setItem("darkMode", "enabled"); 85 | } 86 | 87 | docElement.classList.toggle("dark"); 88 | docElement.classList.toggle("light"); 89 | }; 90 | toggle.addEventListener("click", () => toggleDisplayMode()); 91 | -------------------------------------------------------------------------------- /02_06e/style.css: -------------------------------------------------------------------------------- 1 | /* Default to light color scheme. Respond to manual light setting. */ 2 | :root, 3 | :root.light { 4 | --background-color: white; 5 | --color: black; 6 | --link-color: inherit; 7 | --contrast-color: black; 8 | } 9 | 10 | /* Respond to user settings. */ 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 11%); 14 | --color: hsl(0, 0%, 80%); 15 | --link-color: hsl(219, 100%, 77%); 16 | --contrast-color: hsl(0, 0%, 80%); 17 | } 18 | } 19 | 20 | /* Respond to manual dark setting. */ 21 | :root.dark { 22 | --background-color: hsl(0, 0%, 11%); 23 | --color: hsl(0, 0%, 80%); 24 | --link-color: hsl(219, 100%, 77%); 25 | --contrast-color: hsl(0, 0%, 80%); 26 | } 27 | 28 | body { 29 | background-color: var(--background-color); 30 | color: var(--color); 31 | } 32 | 33 | figure { 34 | margin: 0; 35 | } 36 | 37 | a { 38 | color: var(--link-color); 39 | } 40 | 41 | img { 42 | display: block; 43 | width: 100%; 44 | height: auto; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | align-items: center; 50 | flex-direction: column; 51 | } 52 | 53 | .main-content { 54 | width: min(90vw, 120ch); 55 | margin-block-end: 5rem; 56 | } 57 | 58 | .filler { 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: center; 62 | align-items: center; 63 | height: 100vh; 64 | font-size: 5rem; 65 | } 66 | 67 | /* Card styles */ 68 | .cardlist { 69 | border: 3px solid var(--contrast-color, black); 70 | padding: 1rem; 71 | } 72 | 73 | .cardlist__list { 74 | display: flex; 75 | gap: 1rem; 76 | margin: 0; 77 | padding: 0; 78 | list-style-type: none; 79 | } 80 | 81 | .image { 82 | border: 3px solid var(--contrast-color, black); 83 | } 84 | 85 | .image__caption { 86 | padding: 0.5rem 1rem; 87 | } 88 | 89 | /* Skeleton styles */ 90 | .skeleton { 91 | --skeleton-color: hsl(0, 0%, 75%); 92 | display: grid; 93 | } 94 | 95 | .skeleton .loader, 96 | .skeleton #load, 97 | .skeleton .cardlist__list { 98 | grid-column: 1/1; 99 | grid-row: 1/1; 100 | } 101 | 102 | .skeleton .loader, 103 | .skeleton #load { 104 | z-index: 1; 105 | justify-self: center; 106 | align-self: center; 107 | } 108 | 109 | .skeleton .cardlist__list { 110 | justify-content: space-between; 111 | opacity: 50%; 112 | } 113 | 114 | .skeleton .cardlist__item { 115 | flex-grow: 1; 116 | } 117 | 118 | .skeleton .image { 119 | border-color: var(--skeleton-color); 120 | } 121 | .skeleton .image > div { 122 | width: 100%; 123 | aspect-ratio: 4/5; 124 | background: var(--skeleton-color); 125 | } 126 | 127 | .skeleton h3, 128 | .skeleton p { 129 | content: ""; 130 | width: 100%; 131 | height: 1rem; 132 | background: var(--skeleton-color); 133 | } 134 | 135 | .hidden { 136 | display: none; 137 | } 138 | -------------------------------------------------------------------------------- /02_06e/toggle.css: -------------------------------------------------------------------------------- 1 | /* Accessible toggle. Source: https://kittygiraudel.com/2021/04/05/an-accessible-toggle/ */ 2 | 3 | .toggle { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | position: relative; 8 | margin-bottom: 1em; 9 | cursor: pointer; 10 | gap: 1ch; 11 | } 12 | 13 | .toggle__text { 14 | color: var(--color); 15 | } 16 | button.toggle { 17 | border: 0; 18 | padding: 0; 19 | background-color: transparent; 20 | font: inherit; 21 | } 22 | 23 | .toggle__input { 24 | position: absolute; 25 | opacity: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .toggle__display { 31 | --offset: 0.25em; 32 | --diameter: 1.8em; 33 | 34 | display: inline-flex; 35 | align-items: center; 36 | justify-content: space-around; 37 | box-sizing: content-box; 38 | width: calc(var(--diameter) * 2 + var(--offset) * 2); 39 | height: calc(var(--diameter) + var(--offset) * 2); 40 | border: 0.1em solid rgb(0 0 0 / 0.2); 41 | position: relative; 42 | border-radius: 100vw; 43 | background-color: hsl(0, 0%, 100%); 44 | transition: 250ms; 45 | } 46 | 47 | .toggle__display::before { 48 | content: ""; 49 | z-index: 2; 50 | position: absolute; 51 | top: 50%; 52 | left: var(--offset); 53 | box-sizing: border-box; 54 | width: var(--diameter); 55 | height: var(--diameter); 56 | border: 0.1em solid rgb(0 0 0 / 0.2); 57 | border-radius: 50%; 58 | background-color: rgb(201, 201, 201); 59 | transform: translate(0, -50%); 60 | will-change: transform; 61 | transition: inherit; 62 | } 63 | 64 | .toggle:focus .toggle__display, 65 | .toggle__input:focus + .toggle__display { 66 | outline: 1px dotted #212121; 67 | outline: 1px auto -webkit-focus-ring-color; 68 | outline-offset: 2px; 69 | } 70 | 71 | .toggle:focus, 72 | .toggle:focus:not(:focus-visible) .toggle__display, 73 | .toggle__input:focus:not(:focus-visible) + .toggle__display { 74 | outline: 0; 75 | } 76 | 77 | .toggle[aria-pressed="true"] .toggle__display, 78 | .toggle__input:checked + .toggle__display { 79 | background-color: var(--background-color); 80 | border-color: var(--contrast-color); 81 | } 82 | 83 | .toggle[aria-pressed="true"] .toggle__display::before, 84 | .toggle__input:checked + .toggle__display::before { 85 | transform: translate(100%, -50%); 86 | } 87 | 88 | .toggle[disabled] .toggle__display, 89 | .toggle__input:disabled + .toggle__display { 90 | opacity: 0.6; 91 | filter: grayscale(40%); 92 | cursor: not-allowed; 93 | } 94 | 95 | [dir="rtl"] .toggle__display::before { 96 | left: auto; 97 | right: var(--offset); 98 | } 99 | 100 | [dir="rtl"] .toggle[aria-pressed="true"] + .toggle__display::before, 101 | [dir="rtl"] .toggle__input:checked + .toggle__display::before { 102 | transform: translate(-100%, -50%); 103 | } 104 | 105 | .toggle__icon { 106 | display: inline-block; 107 | width: 1.2em; 108 | height: 1.2em; 109 | color: inherit; 110 | fill: currentcolor; 111 | vertical-align: middle; 112 | overflow: hidden; 113 | } 114 | 115 | .toggle__icon--cross { 116 | color: hsl(48, 97%, 51%); 117 | } 118 | 119 | .toggle__icon--checkmark { 120 | color: hsl(62, 100%, 93%); 121 | } 122 | -------------------------------------------------------------------------------- /03_02b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Current Weather 9 | 10 | 11 |
    12 | 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /03_02b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch weather data from OpenWeather. 3 | * - Store your API key in ./settings.js 4 | * - API reference: https://openweathermap.org/appid 5 | * References: 6 | * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 7 | * - https://developer.mozilla.org/en-US/docs/Web/API/fetch 8 | */ 9 | 10 | import settings from "../settings.js"; 11 | 12 | const tempField = document.querySelector(".getTemp"); 13 | const windSpeed = document.querySelector(".getWSpeed"); 14 | const windDir = document.querySelector(".getWDir"); 15 | -------------------------------------------------------------------------------- /03_02e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Current Weather 9 | 10 | 11 |
    12 | 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /03_02e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch weather data from OpenWeather. 3 | * - Store your API key in ./settings.js 4 | * - API reference: https://openweathermap.org/appid 5 | * References: 6 | * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 7 | * - https://developer.mozilla.org/en-US/docs/Web/API/fetch 8 | */ 9 | 10 | import settings from "../settings.js"; 11 | 12 | const tempField = document.querySelector(".getTemp"); 13 | const windSpeed = document.querySelector(".getWSpeed"); 14 | const windDir = document.querySelector(".getWDir"); 15 | 16 | const displayData = () => { 17 | fetch( 18 | `https://api.openweathermap.org/data/2.5/weather?q=${settings.location}&appid=${settings.appid}` 19 | ) 20 | .then(function (response) { 21 | return response.json(); 22 | }) 23 | .then(function (data) { 24 | console.log(data); 25 | tempField.innerHTML = data.main.temp; 26 | windSpeed.innerHTML = data.wind.speed; 27 | windDir.innerHTML = data.wind.deg; 28 | }); 29 | }; 30 | 31 | displayData(); 32 | -------------------------------------------------------------------------------- /03_03b/components/weathercard.js: -------------------------------------------------------------------------------- 1 | const weatherCard = (data) => { 2 | return ` 3 |
    4 |
    5 |
    ${data.name}, ${ 6 | data.sys.country 7 | }
    8 |
    9 |
    10 | ${ 11 | data.main.temp 12 | }°C 13 |
    14 |
    15 |
    16 | ${ 17 | data.wind.speed 18 | }m/s 19 |
    20 |
    23 | ${data.wind.deg} 24 |
    25 |
    26 |
    27 | `; 28 | }; 29 | 30 | export default weatherCard; 31 | -------------------------------------------------------------------------------- /03_03b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current Weather 10 | 11 | 12 |
    13 |

    Current Weather

    14 |
    15 |
    16 | 17 | 18 |
    19 |
    --
    20 |
    21 |
    22 | --°C 23 |
    24 |
    25 |
    26 | --m/s 27 |
    28 |
    29 |
    30 |
    31 |
    32 | 33 | 34 | -------------------------------------------------------------------------------- /03_03b/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /03_03b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a component to display weather data. 3 | * - Use the new component in the ./components folder 4 | * - Modify the fetch query to call the component. 5 | * - Convert the temperature to metric and fahrenheit 6 | */ 7 | 8 | import settings from "../settings.js"; 9 | 10 | const tempField = document.querySelector(".getTemp"); 11 | const windSpeed = document.querySelector(".getWSpeed"); 12 | const windDir = document.querySelector(".getWDir"); 13 | 14 | const displayData = () => { 15 | fetch( 16 | `https://api.openweathermap.org/data/2.5/weather?q=${settings.location}&appid=${settings.appid}` 17 | ) 18 | .then(function (response) { 19 | return response.json(); 20 | }) 21 | .then(function (data) { 22 | console.log(data); 23 | tempField.innerHTML = data.main.temp; 24 | windSpeed.innerHTML = data.wind.speed; 25 | windDir.innerHTML = data.wind.deg; 26 | }); 27 | }; 28 | 29 | displayData(); 30 | -------------------------------------------------------------------------------- /03_03b/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: white; 3 | --color: black; 4 | --link-color: inherit; 5 | --contrast-color: black; 6 | } 7 | 8 | /* Text meant only for screen readers. */ 9 | .screen-reader-text { 10 | border: 0; 11 | clip: rect(1px, 1px, 1px, 1px); 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | word-wrap: normal !important; 20 | } 21 | 22 | body { 23 | background-color: var(--background-color); 24 | color: var(--color); 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 26 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 27 | } 28 | 29 | figure { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: var(--link-color); 35 | } 36 | 37 | img { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | .container { 44 | display: flex; 45 | align-items: center; 46 | flex-direction: column; 47 | } 48 | 49 | .main-content { 50 | width: min(90vw, 60ch); 51 | } 52 | 53 | .weathercard { 54 | margin-block-start: 2rem; 55 | padding: 1rem; 56 | border: 3px solid hsl(0, 0%, 0%); 57 | border-radius: 1rem; 58 | } 59 | 60 | .weathercard__meta { 61 | display: flex; 62 | justify-content: space-around; 63 | padding: 2rem 0 1rem; 64 | font-size: 1.2rem; 65 | } 66 | 67 | .weathercard__temp { 68 | display: flex; 69 | padding: 3rem; 70 | justify-content: center; 71 | font-size: 8rem; 72 | } 73 | 74 | .weathercard__wind { 75 | display: grid; 76 | justify-content: center; 77 | align-items: center; 78 | padding-block-end: 2rem; 79 | } 80 | 81 | .weathercard__wind::before { 82 | grid-column: 1; 83 | grid-row: 1; 84 | content: ""; 85 | display: block; 86 | margin: 1rem; 87 | height: 4.5rem; 88 | width: 4.5rem; 89 | border-radius: 50%; 90 | border: 6px solid black; 91 | } 92 | 93 | .weathercard__wind-speed { 94 | grid-column: 1; 95 | grid-row: 1; 96 | display: flex; 97 | flex-direction: column; 98 | justify-content: center; 99 | align-items: center; 100 | width: 100%; 101 | } 102 | 103 | .weathercard__wind-dir { 104 | grid-column: 1; 105 | grid-row: 1; 106 | } 107 | 108 | .weathercard__wind-dir::before { 109 | content: "◀"; 110 | display: block; 111 | font-size: 2rem; 112 | width: 7.4rem; 113 | line-height: 1rem; 114 | } 115 | -------------------------------------------------------------------------------- /03_03e/components/weathercard.js: -------------------------------------------------------------------------------- 1 | const tempTranslator = (temp) => { 2 | const allTemps = { 3 | k: temp, 4 | c: temp - 273, 5 | f: 1.8 * (temp - 273) + 32, 6 | }; 7 | console.log(allTemps); 8 | return allTemps; 9 | }; 10 | 11 | const weatherCard = (data) => { 12 | return ` 13 |
    14 |
    15 |
    ${data.name}, ${ 16 | data.sys.country 17 | }
    18 |
    19 |
    20 | ${tempTranslator(data.main.temp).c.toFixed( 21 | 1 22 | )}°C 23 |
    24 |
    25 |
    26 | ${ 27 | data.wind.speed 28 | }m/s 29 |
    30 |
    33 | ${data.wind.deg} 34 |
    35 |
    36 |
    37 | `; 38 | }; 39 | 40 | export default weatherCard; 41 | -------------------------------------------------------------------------------- /03_03e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current Weather 10 | 11 | 12 |
    13 |

    Current Weather

    14 |
    15 |
    16 | 17 | 18 |
    19 |
    --
    20 |
    21 |
    22 | --°C 23 |
    24 |
    25 |
    26 | --m/s 27 |
    28 |
    29 |
    30 |
    31 |
    32 | 33 | 34 | -------------------------------------------------------------------------------- /03_03e/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /03_03e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a component to display weather data. 3 | * - Use the new component in the ./components folder 4 | * - Modify the fetch query to call the component. 5 | * - Convert the temperature to metric and fahrenheit 6 | */ 7 | 8 | import settings from "../settings.js"; 9 | import weatherCard from "./components/weathercard.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | 13 | async function displayData() { 14 | fetch( 15 | `https://api.openweathermap.org/data/2.5/weather?lat=49.2194636514848&lon=-122.96519638087379&APPID=${settings.appid}`, 16 | { 17 | method: "GET", 18 | } 19 | ) 20 | .then(function (response) { 21 | return response.json(); 22 | }) 23 | .then(function (data) { 24 | console.log(data); 25 | mainContent.innerHTML = weatherCard(data); 26 | }); 27 | } 28 | 29 | displayData(); 30 | -------------------------------------------------------------------------------- /03_03e/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: white; 3 | --color: black; 4 | --link-color: inherit; 5 | --contrast-color: black; 6 | } 7 | 8 | /* Text meant only for screen readers. */ 9 | .screen-reader-text { 10 | border: 0; 11 | clip: rect(1px, 1px, 1px, 1px); 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | word-wrap: normal !important; 20 | } 21 | 22 | body { 23 | background-color: var(--background-color); 24 | color: var(--color); 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 26 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 27 | } 28 | 29 | figure { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: var(--link-color); 35 | } 36 | 37 | img { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | .container { 44 | display: flex; 45 | align-items: center; 46 | flex-direction: column; 47 | } 48 | 49 | .main-content { 50 | width: min(90vw, 60ch); 51 | } 52 | 53 | .weathercard { 54 | margin-block-start: 2rem; 55 | padding: 1rem; 56 | border: 3px solid hsl(0, 0%, 0%); 57 | border-radius: 1rem; 58 | } 59 | 60 | .weathercard__meta { 61 | display: flex; 62 | justify-content: space-around; 63 | padding: 2rem 0 1rem; 64 | font-size: 1.2rem; 65 | } 66 | 67 | .weathercard__temp { 68 | display: flex; 69 | padding: 3rem; 70 | justify-content: center; 71 | font-size: 8rem; 72 | } 73 | 74 | .weathercard__wind { 75 | display: grid; 76 | justify-content: center; 77 | align-items: center; 78 | padding-block-end: 2rem; 79 | } 80 | 81 | .weathercard__wind::before { 82 | grid-column: 1; 83 | grid-row: 1; 84 | content: ""; 85 | display: block; 86 | margin: 1rem; 87 | height: 4.5rem; 88 | width: 4.5rem; 89 | border-radius: 50%; 90 | border: 6px solid black; 91 | } 92 | 93 | .weathercard__wind-speed { 94 | grid-column: 1; 95 | grid-row: 1; 96 | display: flex; 97 | flex-direction: column; 98 | justify-content: center; 99 | align-items: center; 100 | width: 100%; 101 | } 102 | 103 | .weathercard__wind-dir { 104 | grid-column: 1; 105 | grid-row: 1; 106 | } 107 | 108 | .weathercard__wind-dir::before { 109 | content: "◀"; 110 | display: block; 111 | font-size: 2rem; 112 | width: 7.4rem; 113 | line-height: 1rem; 114 | } 115 | -------------------------------------------------------------------------------- /03_04b/components/weathercard.js: -------------------------------------------------------------------------------- 1 | const tempTranslator = (temp, unit) => { 2 | const allTemps = { 3 | k: { 4 | value: temp, 5 | unit: "°k", 6 | }, 7 | c: { 8 | value: temp - 273, 9 | unit: "°C", 10 | }, 11 | f: { 12 | value: 1.8 * (temp - 273) + 32, 13 | unit: "°F", 14 | }, 15 | }; 16 | console.log(allTemps); 17 | if (unit === "metric") { 18 | return allTemps.c; 19 | } else if (unit === "imperial") { 20 | return allTemps.f; 21 | } else { 22 | return allTemps.k; 23 | } 24 | }; 25 | 26 | const weatherCard = (data) => { 27 | return ` 28 |
    29 |
    30 |
    ${data.name}, ${ 31 | data.sys.country 32 | }
    33 |
    34 |
    35 | ${ 36 | tempTranslator(data.main.temp).c.toFixed(1) 37 | }°C 38 |
    39 |
    40 |
    41 | ${ 42 | data.wind.speed 43 | }m/s 44 |
    45 |
    48 | ${data.wind.deg} 49 |
    50 |
    51 |
    52 | `; 53 | }; 54 | 55 | export default weatherCard; 56 | -------------------------------------------------------------------------------- /03_04b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current Weather 10 | 11 | 12 |
    13 |

    Current Weather

    14 |
    15 |
    16 | 17 | 18 |
    19 |
    --
    20 |
    21 |
    22 | --°C 23 |
    24 |
    25 |
    26 | --m/s 27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | 34 | 35 | -------------------------------------------------------------------------------- /03_04b/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /03_04b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a button to change units. 3 | * - Get the default unit type from settings.js. 4 | * - Create data for imperial and metric units. 5 | * - Create a button for switching between imperial and metric units. 6 | */ 7 | 8 | import settings from "../settings.js"; 9 | import weatherCard from "./components/weathercard.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | 13 | async function displayData() { 14 | fetch( 15 | `https://api.openweathermap.org/data/2.5/weather?lat=49.2194636514848&lon=-122.96519638087379&APPID=${settings.appid}`, 16 | { 17 | method: "GET", 18 | } 19 | ) 20 | .then(function (response) { 21 | return response.json(); 22 | }) 23 | .then(function (data) { 24 | console.log(data); 25 | mainContent.innerHTML = weatherCard(data); 26 | }); 27 | } 28 | 29 | displayData(); 30 | -------------------------------------------------------------------------------- /03_04b/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: white; 3 | --color: black; 4 | --link-color: inherit; 5 | --contrast-color: black; 6 | } 7 | 8 | /* Text meant only for screen readers. */ 9 | .screen-reader-text { 10 | border: 0; 11 | clip: rect(1px, 1px, 1px, 1px); 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | word-wrap: normal !important; 20 | } 21 | 22 | body { 23 | background-color: var(--background-color); 24 | color: var(--color); 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 26 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 27 | } 28 | 29 | figure { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: var(--link-color); 35 | } 36 | 37 | img { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | .container { 44 | display: flex; 45 | align-items: center; 46 | flex-direction: column; 47 | } 48 | 49 | .main-content { 50 | width: min(90vw, 60ch); 51 | } 52 | 53 | .weathercard { 54 | margin-block-start: 2rem; 55 | padding: 1rem; 56 | border: 3px solid hsl(0, 0%, 0%); 57 | border-radius: 1rem; 58 | } 59 | 60 | .weathercard__meta { 61 | display: flex; 62 | justify-content: space-around; 63 | padding: 2rem 0 1rem; 64 | font-size: 1.2rem; 65 | } 66 | 67 | .weathercard__temp { 68 | display: flex; 69 | padding: 3rem; 70 | justify-content: center; 71 | font-size: 8rem; 72 | } 73 | 74 | .weathercard__wind { 75 | display: grid; 76 | justify-content: center; 77 | align-items: center; 78 | padding-block-end: 2rem; 79 | } 80 | 81 | .weathercard__wind::before { 82 | grid-column: 1; 83 | grid-row: 1; 84 | content: ""; 85 | display: block; 86 | margin: 1rem; 87 | height: 4.5rem; 88 | width: 4.5rem; 89 | border-radius: 50%; 90 | border: 6px solid black; 91 | } 92 | 93 | .weathercard__wind-speed { 94 | grid-column: 1; 95 | grid-row: 1; 96 | display: flex; 97 | flex-direction: column; 98 | justify-content: center; 99 | align-items: center; 100 | width: 100%; 101 | } 102 | 103 | .weathercard__wind-dir { 104 | grid-column: 1; 105 | grid-row: 1; 106 | } 107 | 108 | .weathercard__wind-dir::before { 109 | content: "◀"; 110 | display: block; 111 | font-size: 2rem; 112 | width: 7.4rem; 113 | line-height: 1rem; 114 | } 115 | -------------------------------------------------------------------------------- /03_04e/components/weathercard.js: -------------------------------------------------------------------------------- 1 | const tempTranslator = (temp, unit) => { 2 | const allTemps = { 3 | k: { 4 | value: temp, 5 | unit: "°k", 6 | }, 7 | c: { 8 | value: temp - 273, 9 | unit: "°C", 10 | }, 11 | f: { 12 | value: 1.8 * (temp - 273) + 32, 13 | unit: "°F", 14 | }, 15 | }; 16 | console.log(allTemps); 17 | if (unit === "metric") { 18 | return allTemps.c; 19 | } else if (unit === "imperial") { 20 | return allTemps.f; 21 | } else { 22 | return allTemps.k; 23 | } 24 | }; 25 | 26 | const speedTranslator = (speed, units) => { 27 | const allSpeeds = { 28 | metric: { 29 | value: speed, 30 | unit: "m/s", 31 | }, 32 | imperial: { 33 | value: speed * 3.281, 34 | unit: "ft/s", 35 | }, 36 | }; 37 | if (units === "metric") { 38 | return allSpeeds.metric; 39 | } else if (units === "imperial") { 40 | return allSpeeds.imperial; 41 | } else { 42 | return allSpeeds.metric; 43 | } 44 | }; 45 | 46 | const weatherCard = (data, units) => { 47 | return ` 48 |
    49 |
    50 |
    ${data.name}, ${ 51 | data.sys.country 52 | }
    53 |
    54 |
    55 | ${tempTranslator( 56 | data.main.temp, 57 | units 58 | ).value.toFixed(1)}${ 59 | tempTranslator(data.main.temp, units).unit 60 | } 61 |
    62 |
    63 |
    64 | ${speedTranslator( 65 | data.wind.speed, 66 | units 67 | ).value.toFixed(1)}${ 68 | speedTranslator(data.wind.speed, units).unit 69 | } 70 |
    71 |
    74 | ${data.wind.deg} 75 |
    76 |
    77 | 78 |
    79 | `; 80 | }; 81 | 82 | export default weatherCard; 83 | -------------------------------------------------------------------------------- /03_04e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current Weather 10 | 11 | 12 |
    13 |

    Current Weather

    14 |
    15 |
    16 | 17 | 18 |
    19 |
    --
    20 |
    21 |
    22 | --°C 23 |
    24 |
    25 |
    26 | --m/s 27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | 34 | 35 | -------------------------------------------------------------------------------- /03_04e/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /03_04e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a button to change units. 3 | * - Get the default unit type from settings.js. 4 | * - Create data for imperial and metric units. 5 | * - Create a button for switching between imperial and metric units. 6 | */ 7 | 8 | import settings from "../settings.js"; 9 | import weatherCard from "./components/weathercard.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | let units = settings.units; 13 | 14 | const unitChanger = () => { 15 | const unitsButton = document.querySelector("#units"); 16 | unitsButton.addEventListener("click", () => { 17 | units === "metric" ? (units = "imperial") : (units = "metric"); 18 | displayData(units); 19 | }); 20 | }; 21 | 22 | async function displayData(units) { 23 | fetch( 24 | `https://api.openweathermap.org/data/2.5/weather?q=carpinteria,ca,us&APPID=${settings.appid}`, 25 | { 26 | method: "GET", 27 | } 28 | ) 29 | .then(function (response) { 30 | return response.json(); 31 | }) 32 | .then(function (data) { 33 | console.log(data); 34 | mainContent.innerHTML = weatherCard(data, units); 35 | }) 36 | .then(function () { 37 | unitChanger(); 38 | }); 39 | } 40 | 41 | displayData(units); 42 | -------------------------------------------------------------------------------- /03_04e/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: white; 3 | --color: black; 4 | --link-color: inherit; 5 | --contrast-color: black; 6 | } 7 | 8 | /* Text meant only for screen readers. */ 9 | .screen-reader-text { 10 | border: 0; 11 | clip: rect(1px, 1px, 1px, 1px); 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | word-wrap: normal !important; 20 | } 21 | 22 | body { 23 | background-color: var(--background-color); 24 | color: var(--color); 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 26 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 27 | } 28 | 29 | figure { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: var(--link-color); 35 | } 36 | 37 | img { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | .container { 44 | display: flex; 45 | align-items: center; 46 | flex-direction: column; 47 | } 48 | 49 | .main-content { 50 | width: min(90vw, 60ch); 51 | } 52 | 53 | .weathercard { 54 | margin-block-start: 2rem; 55 | padding: 1rem; 56 | border: 3px solid hsl(0, 0%, 0%); 57 | border-radius: 1rem; 58 | } 59 | 60 | .weathercard__meta { 61 | display: flex; 62 | justify-content: space-around; 63 | padding: 2rem 0 1rem; 64 | font-size: 1.2rem; 65 | } 66 | 67 | .weathercard__temp { 68 | display: flex; 69 | padding: 3rem; 70 | justify-content: center; 71 | font-size: 8rem; 72 | } 73 | 74 | .weathercard__wind { 75 | display: grid; 76 | justify-content: center; 77 | align-items: center; 78 | padding-block-end: 2rem; 79 | } 80 | 81 | .weathercard__wind::before { 82 | grid-column: 1; 83 | grid-row: 1; 84 | content: ""; 85 | display: block; 86 | margin: 1rem; 87 | height: 4.5rem; 88 | width: 4.5rem; 89 | border-radius: 50%; 90 | border: 6px solid black; 91 | } 92 | 93 | .weathercard__wind-speed { 94 | grid-column: 1; 95 | grid-row: 1; 96 | display: flex; 97 | flex-direction: column; 98 | justify-content: center; 99 | align-items: center; 100 | width: 100%; 101 | } 102 | 103 | .weathercard__wind-dir { 104 | grid-column: 1; 105 | grid-row: 1; 106 | } 107 | 108 | .weathercard__wind-dir::before { 109 | content: "◀"; 110 | display: block; 111 | font-size: 2rem; 112 | width: 7.4rem; 113 | line-height: 1rem; 114 | } 115 | -------------------------------------------------------------------------------- /03_05b/components/weathercard.js: -------------------------------------------------------------------------------- 1 | const tempTranslator = (temp, unit) => { 2 | const allTemps = { 3 | k: { 4 | value: temp, 5 | unit: "°k", 6 | }, 7 | c: { 8 | value: temp - 273, 9 | unit: "°C", 10 | }, 11 | f: { 12 | value: 1.8 * (temp - 273) + 32, 13 | unit: "°F", 14 | }, 15 | }; 16 | console.log(allTemps); 17 | if (unit === "metric") { 18 | return allTemps.c; 19 | } else if (unit === "imperial") { 20 | return allTemps.f; 21 | } else { 22 | return allTemps.k; 23 | } 24 | }; 25 | 26 | const speedTranslator = (speed, units) => { 27 | const allSpeeds = { 28 | metric: { 29 | value: speed, 30 | unit: "m/s", 31 | }, 32 | imperial: { 33 | value: speed * 3.281, 34 | unit: "ft/s", 35 | }, 36 | }; 37 | if (units === "metric") { 38 | return allSpeeds.metric; 39 | } else if (units === "imperial") { 40 | return allSpeeds.imperial; 41 | } else { 42 | return allSpeeds.metric; 43 | } 44 | }; 45 | 46 | const weatherCard = (data, units) => { 47 | return ` 48 |
    49 |
    50 |
    ${data.name}, ${ 51 | data.sys.country 52 | }
    53 |
    54 |
    55 | ${tempTranslator( 56 | data.main.temp, 57 | units 58 | ).value.toFixed(1)}${ 59 | tempTranslator(data.main.temp, units).unit 60 | } 61 |
    62 |
    63 |
    64 | ${speedTranslator( 65 | data.wind.speed, 66 | units 67 | ).value.toFixed(1)}${ 68 | speedTranslator(data.wind.speed, units).unit 69 | } 70 |
    71 |
    74 | ${data.wind.deg} 75 |
    76 |
    77 | 78 |
    79 | `; 80 | }; 81 | 82 | export default weatherCard; 83 | -------------------------------------------------------------------------------- /03_05b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current Weather 10 | 11 | 12 |
    13 |

    Current Weather

    14 | 15 |
    16 |
    17 | 18 | 19 | 20 | 29 | 30 |
    31 | 32 |

    33 | For USA, enter "[city],[state],US" where [state] is the two-letter 34 | abbreviation. For every other country, enter "[city],[country]" where 35 | [country] is the two-letter abbreviation. 36 |

    37 |
    38 | 39 |
    40 |
    41 | 42 | 43 |
    44 |
    --
    45 |
    46 |
    47 | --°C 48 |
    49 |
    50 |
    51 | --m/s 52 |
    53 |
    54 | 55 |
    56 |
    57 |
    58 | 59 | 60 | -------------------------------------------------------------------------------- /03_05b/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /03_05b/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a location selector. 3 | * - Capture the location field input. 4 | * - Send a fetch query to the OpenWeather geocoding API (https://openweathermap.org/api/geocoding-api) 5 | * - Use the lat and lon data from the geocoding response to fetch weather data. 6 | */ 7 | 8 | import settings from "../settings.js"; 9 | import weatherCard from "./components/weathercard.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | const locationForm = document.querySelector(".locationform"); 13 | const formInput = document.querySelector("#location"); 14 | let location = settings.location; 15 | let units = settings.units; 16 | const errorMsg = document.querySelector(".error"); 17 | 18 | // Caputre location form submit 19 | locationForm.addEventListener("submit", (event) => { 20 | event.preventDefault(); 21 | errorMsg.classList.add("hidden"); 22 | console.log(formInput.value); 23 | location = formInput.value; 24 | displayData(location, units); 25 | }); 26 | 27 | const unitChanger = () => { 28 | const unitsButton = document.querySelector("#units"); 29 | unitsButton.addEventListener("click", () => { 30 | units === "metric" ? (units = "imperial") : (units = "metric"); 31 | displayData(units); 32 | }); 33 | }; 34 | 35 | async function displayData(location, units) { 36 | fetch( 37 | `https://api.openweathermap.org/data/2.5/weather?q=${location}&APPID=${settings.appid}` 38 | ) 39 | .then(function (response) { 40 | return response.json(); 41 | }) 42 | .then(function (data) { 43 | console.log(data); 44 | mainContent.innerHTML = weatherCard(data, units); 45 | }) 46 | .then(function () { 47 | unitChanger(); 48 | }); 49 | } 50 | 51 | displayData(location, units); 52 | -------------------------------------------------------------------------------- /03_05b/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: white; 3 | --color: black; 4 | --link-color: inherit; 5 | --contrast-color: black; 6 | } 7 | 8 | /* Text meant only for screen readers. */ 9 | .screen-reader-text { 10 | border: 0; 11 | clip: rect(1px, 1px, 1px, 1px); 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | word-wrap: normal !important; 20 | } 21 | 22 | body { 23 | background-color: var(--background-color); 24 | color: var(--color); 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 26 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 27 | } 28 | 29 | figure { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: var(--link-color); 35 | } 36 | 37 | img { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | .container { 44 | display: flex; 45 | align-items: center; 46 | flex-direction: column; 47 | } 48 | 49 | .main-content, 50 | .locationform { 51 | width: min(90vw, 60ch); 52 | } 53 | 54 | .locationform__elements { 55 | display: flex; 56 | gap: 1rem; 57 | justify-content: center; 58 | } 59 | 60 | .error { 61 | color: hsl(0, 100%, 23%); 62 | background-color: hsla(0, 100%, 50%, 0.2); 63 | } 64 | 65 | .hidden { 66 | display: none; 67 | } 68 | 69 | .instructions { 70 | font-style: italic; 71 | font-size: 90%; 72 | color: hsl(0, 0%, 30%); 73 | } 74 | 75 | .weathercard { 76 | margin-block-start: 2rem; 77 | padding: 1rem; 78 | border: 3px solid hsl(0, 0%, 0%); 79 | border-radius: 1rem; 80 | } 81 | 82 | .weathercard__meta { 83 | display: flex; 84 | justify-content: space-around; 85 | padding: 2rem 0 1rem; 86 | font-size: 1.2rem; 87 | } 88 | 89 | .weathercard__temp { 90 | display: flex; 91 | padding: 3rem; 92 | justify-content: center; 93 | font-size: 8rem; 94 | } 95 | 96 | .weathercard__wind { 97 | display: grid; 98 | justify-content: center; 99 | align-items: center; 100 | padding-block-end: 2rem; 101 | } 102 | 103 | .weathercard__wind::before { 104 | grid-column: 1; 105 | grid-row: 1; 106 | content: ""; 107 | display: block; 108 | margin: 1rem; 109 | height: 4.5rem; 110 | width: 4.5rem; 111 | border-radius: 50%; 112 | border: 6px solid black; 113 | } 114 | 115 | .weathercard__wind-speed { 116 | grid-column: 1; 117 | grid-row: 1; 118 | display: flex; 119 | flex-direction: column; 120 | justify-content: center; 121 | align-items: center; 122 | width: 100%; 123 | } 124 | 125 | .weathercard__wind-dir { 126 | grid-column: 1; 127 | grid-row: 1; 128 | } 129 | 130 | .weathercard__wind-dir::before { 131 | content: "◀"; 132 | display: block; 133 | font-size: 2rem; 134 | width: 7.4rem; 135 | line-height: 1rem; 136 | } 137 | -------------------------------------------------------------------------------- /03_05e/components/weathercard.js: -------------------------------------------------------------------------------- 1 | const tempTranslator = (temp, unit) => { 2 | const allTemps = { 3 | k: { 4 | value: temp, 5 | unit: "°k", 6 | }, 7 | c: { 8 | value: temp - 273, 9 | unit: "°C", 10 | }, 11 | f: { 12 | value: 1.8 * (temp - 273) + 32, 13 | unit: "°F", 14 | }, 15 | }; 16 | console.log(allTemps); 17 | if (unit === "metric") { 18 | return allTemps.c; 19 | } else if (unit === "imperial") { 20 | return allTemps.f; 21 | } else { 22 | return allTemps.k; 23 | } 24 | }; 25 | 26 | const speedTranslator = (speed, units) => { 27 | const allSpeeds = { 28 | metric: { 29 | value: speed, 30 | unit: "m/s", 31 | }, 32 | imperial: { 33 | value: speed * 3.281, 34 | unit: "ft/s", 35 | }, 36 | }; 37 | if (units === "metric") { 38 | return allSpeeds.metric; 39 | } else if (units === "imperial") { 40 | return allSpeeds.imperial; 41 | } else { 42 | return allSpeeds.metric; 43 | } 44 | }; 45 | 46 | const weatherCard = (data, units) => { 47 | return ` 48 |
    49 |
    50 |
    ${data.name}, ${ 51 | data.sys.country 52 | }
    53 |
    54 |
    55 | ${tempTranslator( 56 | data.main.temp, 57 | units 58 | ).value.toFixed(1)}${ 59 | tempTranslator(data.main.temp, units).unit 60 | } 61 |
    62 |
    63 |
    64 | ${speedTranslator( 65 | data.wind.speed, 66 | units 67 | ).value.toFixed(1)}${ 68 | speedTranslator(data.wind.speed, units).unit 69 | } 70 |
    71 |
    74 | ${data.wind.deg} 75 |
    76 |
    77 | 78 |
    79 | `; 80 | }; 81 | 82 | export default weatherCard; 83 | -------------------------------------------------------------------------------- /03_05e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Current Weather 10 | 11 | 12 |
    13 |

    Current Weather

    14 | 15 |
    16 |
    17 | 18 | 19 | 20 | 29 | 30 |
    31 | 32 |

    33 | For USA, enter "[city],[state],US" where [state] is the two-letter 34 | abbreviation. For every other country, enter "[city],[country]" where 35 | [country] is the two-letter abbreviation. 36 |

    37 |
    38 | 39 |
    40 |
    41 | 42 | 43 |
    44 |
    --
    45 |
    46 |
    47 | --°C 48 |
    49 |
    50 |
    51 | --m/s 52 |
    53 |
    54 | 55 |
    56 |
    57 |
    58 | 59 | 60 | -------------------------------------------------------------------------------- /03_05e/loader.css: -------------------------------------------------------------------------------- 1 | /* Source: https://cssloaders.github.io/ */ 2 | .loader { 3 | width: 48px; 4 | height: 48px; 5 | border-radius: 50%; 6 | position: relative; 7 | animation: rotate 1s linear infinite; 8 | } 9 | .loader::before { 10 | content: ""; 11 | box-sizing: border-box; 12 | position: absolute; 13 | inset: 0px; 14 | border-radius: 50%; 15 | border: 5px solid hsl(0, 0%, 0%); 16 | animation: prixClipFix 2s linear infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes prixClipFix { 26 | 0% { 27 | clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); 28 | } 29 | 25% { 30 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); 31 | } 32 | 50% { 33 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); 34 | } 35 | 75% { 36 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); 37 | } 38 | 100% { 39 | clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /03_05e/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a location selector. 3 | * - Capture the location field input. 4 | * - Send a fetch query to the OpenWeather geocoding API (https://openweathermap.org/api/geocoding-api) 5 | * - Use the lat and lon data from the geocoding response to fetch weather data. 6 | */ 7 | 8 | import settings from "../settings.js"; 9 | import weatherCard from "./components/weathercard.js"; 10 | 11 | const mainContent = document.querySelector(".main-content"); 12 | const locationForm = document.querySelector(".locationform"); 13 | const formInput = document.querySelector("#location"); 14 | let location = settings.location; 15 | let units = settings.units; 16 | const errorMsg = document.querySelector(".error"); 17 | 18 | // Caputre location form submit 19 | locationForm.addEventListener("submit", (event) => { 20 | event.preventDefault(); 21 | errorMsg.classList.add("hidden"); 22 | console.log(formInput.value); 23 | location = formInput.value; 24 | displayData(location, units); 25 | }); 26 | 27 | const unitChanger = () => { 28 | const unitsButton = document.querySelector("#units"); 29 | unitsButton.addEventListener("click", () => { 30 | units === "metric" ? (units = "imperial") : (units = "metric"); 31 | displayData(location, units); 32 | }); 33 | }; 34 | 35 | async function displayData(location, units) { 36 | const currentLoc = await fetch( 37 | `https://api.openweathermap.org/geo/1.0/direct?q=${location}&limit=1&APPID=${settings.appid}` 38 | ) 39 | .then((response) => response.json()) 40 | .then(function (data) { 41 | if (data.length === 0) { 42 | errorMsg.classList.remove("hidden"); 43 | errorMsg.innerHTML = "No location by that name. Try again."; 44 | } else { 45 | return data; 46 | } 47 | }) 48 | .catch((error) => { 49 | errorMsg.classList.remove("hidden"); 50 | errorMsg.innerHTML = "Something went wrong. Try again."; 51 | console.error("Location query error:", error); 52 | }); 53 | 54 | if (currentLoc) { 55 | fetch( 56 | `https://api.openweathermap.org/data/2.5/weather?lat=${currentLoc[0].lat}&lon=${currentLoc[0].lon}&APPID=${settings.appid}` 57 | ) 58 | .then(function (response) { 59 | return response.json(); 60 | }) 61 | .then(function (data) { 62 | console.log(data); 63 | mainContent.innerHTML = weatherCard(data, units); 64 | }) 65 | .then(function () { 66 | unitChanger(); 67 | }) 68 | .catch((error) => { 69 | console.error("Weather query error:", error); 70 | }); 71 | } 72 | } 73 | 74 | displayData(location, units); 75 | -------------------------------------------------------------------------------- /03_05e/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: white; 3 | --color: black; 4 | --link-color: inherit; 5 | --contrast-color: black; 6 | } 7 | 8 | /* Text meant only for screen readers. */ 9 | .screen-reader-text { 10 | border: 0; 11 | clip: rect(1px, 1px, 1px, 1px); 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | word-wrap: normal !important; 20 | } 21 | 22 | body { 23 | background-color: var(--background-color); 24 | color: var(--color); 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 26 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 27 | } 28 | 29 | figure { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: var(--link-color); 35 | } 36 | 37 | img { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | .container { 44 | display: flex; 45 | align-items: center; 46 | flex-direction: column; 47 | } 48 | 49 | .main-content, 50 | .locationform { 51 | width: min(90vw, 60ch); 52 | } 53 | 54 | .locationform__elements { 55 | display: flex; 56 | gap: 1rem; 57 | justify-content: center; 58 | } 59 | 60 | .error { 61 | color: hsl(0, 100%, 23%); 62 | background-color: hsla(0, 100%, 50%, 0.2); 63 | } 64 | 65 | .hidden { 66 | display: none; 67 | } 68 | 69 | .instructions { 70 | font-style: italic; 71 | font-size: 90%; 72 | color: hsl(0, 0%, 30%); 73 | } 74 | 75 | .weathercard { 76 | margin-block-start: 2rem; 77 | padding: 1rem; 78 | border: 3px solid hsl(0, 0%, 0%); 79 | border-radius: 1rem; 80 | } 81 | 82 | .weathercard__meta { 83 | display: flex; 84 | justify-content: space-around; 85 | padding: 2rem 0 1rem; 86 | font-size: 1.2rem; 87 | } 88 | 89 | .weathercard__temp { 90 | display: flex; 91 | padding: 3rem; 92 | justify-content: center; 93 | font-size: 8rem; 94 | } 95 | 96 | .weathercard__wind { 97 | display: grid; 98 | justify-content: center; 99 | align-items: center; 100 | padding-block-end: 2rem; 101 | } 102 | 103 | .weathercard__wind::before { 104 | grid-column: 1; 105 | grid-row: 1; 106 | content: ""; 107 | display: block; 108 | margin: 1rem; 109 | height: 4.5rem; 110 | width: 4.5rem; 111 | border-radius: 50%; 112 | border: 6px solid black; 113 | } 114 | 115 | .weathercard__wind-speed { 116 | grid-column: 1; 117 | grid-row: 1; 118 | display: flex; 119 | flex-direction: column; 120 | justify-content: center; 121 | align-items: center; 122 | width: 100%; 123 | } 124 | 125 | .weathercard__wind-dir { 126 | grid-column: 1; 127 | grid-row: 1; 128 | } 129 | 130 | .weathercard__wind-dir::before { 131 | content: "◀"; 132 | display: block; 133 | font-size: 2rem; 134 | width: 7.4rem; 135 | line-height: 1rem; 136 | } 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contribution Agreement 3 | ====================== 4 | 5 | This repository does not accept pull requests (PRs). All pull requests will be closed. 6 | 7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license. 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the LinkedIn Learning Exercise File License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | Please note, this project may automatically load third party code from external 8 | repositories (for example, NPM modules, Composer packages, or other dependencies). 9 | If so, such third party code may be subject to other license terms than as set 10 | forth above. In addition, such third party code may also depend on and load 11 | multiple tiers of dependencies. Please review the applicable licenses of the 12 | additional dependencies. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hands-On Introduction: JavaScript 2 | This is the repository for the LinkedIn Learning course Hands-On Introduction: JavaScript. The full course is available from [LinkedIn Learning][lil-course-url]. 3 | 4 | ![1666989901003](https://user-images.githubusercontent.com/28540243/200742696-e631d384-f572-4306-8283-0fc456243b82.jpeg) 5 | 6 | Are you brand-new to coding in JavaScript? Or are you just looking to learn a little bit more? Sometimes, when you learn a new framework, you need to jump right in and get things done. Join LinkedIn Learning senior instructor Morten Rand-Hendriksen as he shows you what it takes to get started writing JavaScript without trying to familiarize yourself with it beforehand. Explore this interactive, easy-to-follow primer on making changes directly to your code, testing out your new skills as you go, and making edits on your own in real time. Get tips from Morten on picking JS apart, building code from scratch, and then extending it, as well as fetching data from APIs to build new JS components that allow you to use your data. By the end of this course, you’ll be ready to get your hands dirty with your own code, no matter your level of experience or professional background.

    The best way to learn a language is to use it in practice. That’s why this course is integrated with GitHub Codespaces, an instant cloud developer environment that offers all the functionality of your favorite IDE without the need for any local machine setup. With GitHub Codespaces, you can get hands-on practice from any machine, at any time—all while using a tool that you’ll likely encounter in the workplace. Check out the [Using GitHub Codespaces with this course][gcs-video-url] video to learn how to get started. 7 | 8 | ### Instructor 9 | 10 | Morten Rand-Hendricksen 11 | 12 | Check out my other courses on [LinkedIn Learning](https://www.linkedin.com/learning/instructors/morten-rand-hendriksen). 13 | 14 | [lil-course-url]: https://www.linkedin.com/learning/hands-on-introduction-javascript 15 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/D560DAQHc8xeoOVTOnQ/learning-public-crop_675_1200/0/1666989901003?e=1667952000&v=beta&t=AD6IwBmB-t2zy1ocP6QPDvC-NVC5808WPF5v8nbg7nU 16 | [gcs-video-url]: https://www.linkedin.com/learning/hands-on-introduction-javascript/using-github-codespaces-with-this-course 17 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/hands-on-javascript-2499547/11e1274db5f9954f812847ec0fac6cef7302285e/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hands-on-js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "eslint": "^8.20.0", 13 | "eslint-config-prettier": "^8.5.0", 14 | "eslint-config-standard": "^17.0.0", 15 | "eslint-plugin-import": "^2.26.0", 16 | "eslint-plugin-n": "^15.2.4", 17 | "eslint-plugin-promise": "^6.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Get API key from https://home.openweathermap.org/users/sign_up 3 | appid: [OpenWeatherAPIkey], 4 | units: "metric", 5 | location: "carpinteria,ca,us", 6 | }; 7 | --------------------------------------------------------------------------------