├── .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 |
30 |
31 | ${imgData.description}
32 |
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 |
31 |
32 | ${imgData.description}
33 |
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 |
30 |
31 | ${imgData.description}
32 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
18 | View mode:
19 |
20 |
28 |
38 |
39 |
40 |
51 |
59 |
60 |
61 |
65 |
66 |
69 |
70 |
71 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
18 | View mode:
19 |
20 |
28 |
38 |
39 |
40 |
51 |
59 |
60 |
61 |
65 |
66 |
69 |
70 |
71 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
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 |
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 |
14 | ${data.map((imgData) => cardListItem(imgData)).join("")}
15 |
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 |
13 | Temperature: N/A K
14 | Wind speed: N/A m/s
15 | Wind direction: N/A degrees
16 |
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 |
13 | Temperature: N/A K
14 | Wind speed: N/A m/s
15 | Wind direction: N/A degrees
16 |
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 |
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 |
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 |
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 |
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 |
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 |
21 |
22 | -- °C
23 |
24 |
25 |
26 | -- m/s
27 |
28 |
29 | Change units
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 |
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 | Change units
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 |
21 |
22 | -- °C
23 |
24 |
25 |
26 | -- m/s
27 |
28 |
29 | Change units
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 |
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 | Change units
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 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 | -- °C
48 |
49 |
50 |
51 | -- m/s
52 |
53 |
54 | Change units
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 |
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 | Change units
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 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 | -- °C
48 |
49 |
50 |
51 | -- m/s
52 |
53 |
54 | Change units
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 | 
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 |
--------------------------------------------------------------------------------