Browse Source

Create a copy of the dom tests to js image

pull/1327/head
Clement Denis 2 years ago committed by Clément
parent
commit
8573f049f2
  1. 19
      js/tests/Dockerfile
  2. 45
      js/tests/action-reaction-dom_test.mjs
  3. 43
      js/tests/bring-it-to-life-dom_test.mjs
  4. 109
      js/tests/build-brick-and-break-dom_test.mjs
  5. 36
      js/tests/class-that-dom_test.mjs
  6. 71
      js/tests/fifty-shades-of-cold-dom_test.mjs
  7. 46
      js/tests/first-words-dom_test.mjs
  8. 127
      js/tests/get-them-all-dom_test.mjs
  9. 123
      js/tests/gossip-grid-dom_test.mjs
  10. 67
      js/tests/harder-bigger-bolder-stronger-dom_test.mjs
  11. 23
      js/tests/hello-there_test.js
  12. 55
      js/tests/keycodes-symphony-dom_test.mjs
  13. 122
      js/tests/mouse-trap-dom_test.mjs
  14. 90
      js/tests/nesting-organs-dom_test.mjs
  15. 110
      js/tests/pick-and-click-dom_test.mjs
  16. 36
      js/tests/pimp-my-style-dom_test.mjs
  17. 49
      js/tests/select-and-style-dom_test.mjs
  18. 25
      js/tests/skeleton-dom_test.mjs
  19. 164
      js/tests/test.mjs
  20. 19
      js/tests/the-calling-dom_test.mjs
  21. 174
      js/tests/where-do-we-go-dom_test.mjs
  22. 81
      subjects/action-reaction-dom/README.md
  23. 50
      subjects/bring-it-to-life-dom/README.md
  24. BIN
      subjects/bring-it-to-life-dom/bring-it-to-life.png
  25. 44
      subjects/build-brick-and-break-dom/README.md
  26. 108
      subjects/build-brick-and-break-dom/build-brick-and-break.html
  27. 57
      subjects/class-that-dom/README.md
  28. BIN
      subjects/class-that-dom/class-that.png
  29. 46
      subjects/fifty-shades-of-cold-dom/README.md
  30. 143
      subjects/fifty-shades-of-cold-dom/fifty-shades-of-cold.data.js
  31. 61
      subjects/fifty-shades-of-cold-dom/fifty-shades-of-cold.html
  32. 72
      subjects/first-words-dom/README.md
  33. 53
      subjects/get-them-all-dom/README.md
  34. 47
      subjects/get-them-all-dom/get-them-all.data.js
  35. 179
      subjects/get-them-all-dom/get-them-all.html
  36. 38
      subjects/gossip-grid-dom/README.md
  37. 19
      subjects/gossip-grid-dom/gossip-grid.data.js
  38. 135
      subjects/gossip-grid-dom/gossip-grid.html
  39. 28
      subjects/harder-bigger-bolder-stronger-dom/README.md
  40. 57
      subjects/harder-bigger-bolder-stronger-dom/harder-bigger-bolder-stronger.html
  41. 29
      subjects/keycodes-symphony-dom/README.md
  42. 57
      subjects/keycodes-symphony-dom/keycodes-symphony.html
  43. 35
      subjects/mouse-trap-dom/README.md
  44. 63
      subjects/mouse-trap-dom/mouse-trap.html
  45. 79
      subjects/nesting-organs-dom/README.md
  46. BIN
      subjects/nesting-organs-dom/nesting-organs.png
  47. 44
      subjects/pick-and-click-dom/README.md
  48. 86
      subjects/pick-and-click-dom/pick-and-click.html
  49. 55
      subjects/pimp-my-style-dom/README.md
  50. 17
      subjects/pimp-my-style-dom/pimp-my-style.data.js
  51. 181
      subjects/pimp-my-style-dom/pimp-my-style.html
  52. 81
      subjects/select-and-style-dom/README.md
  53. BIN
      subjects/select-and-style-dom/select-and-style.png
  54. 81
      subjects/skeleton-dom/README.md
  55. 40
      subjects/the-calling-dom/README.md
  56. 43
      subjects/where-do-we-go-dom/README.md
  57. 107
      subjects/where-do-we-go-dom/where-do-we-go.data.js
  58. 72
      subjects/where-do-we-go-dom/where-do-we-go.html
  59. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/almeria.jpg
  60. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/arlit.jpg
  61. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/atlanta.jpg
  62. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/black-rock-desert.jpg
  63. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/cordoba.jpg
  64. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/georgetown.jpg
  65. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/killeen.jpg
  66. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/lisse.jpg
  67. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/los-caracoles-pass.jpg
  68. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/marrakesh.jpg
  69. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/moab.jpg
  70. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/mount-fuji.jpg
  71. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/namib-desert.jpg
  72. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/newark.jpg
  73. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/nishinoshima-island.jpg
  74. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/qinhuangdao.jpg
  75. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/rio-de-janeiro.jpg
  76. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/shadegan-lagoon.jpg
  77. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/skafta-river.jpg
  78. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/tucson.jpg
  79. BIN
      subjects/where-do-we-go-dom/where-do-we-go_images/yuanyang-county.jpg

19
js/tests/Dockerfile

@ -1,4 +1,21 @@
FROM node:14.16.0-alpine3.12
FROM alpine:3.16.0
# Installs latest Chromium package.
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn
# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
RUN yarn add puppeteer@15.3.2
WORKDIR /app
COPY . .

45
js/tests/action-reaction-dom_test.mjs

@ -0,0 +1,45 @@
export const tests = []
tests.push(async ({ eq, page }) => {
// check the initial class name of the eye left
const eyeLeft = await page.$eval('#eye-left', (node) => node.className)
eq(eyeLeft, 'eye')
// check that the text of the button says 'close'
const buttonText = await page.$eval('button', (node) => node.textContent)
eq(buttonText, 'Click to close the left eye')
})
tests.push(async ({ eq, page }) => {
// click the button to close the left eye
const button = await page.$('button')
button.click()
// check that the class has been added
await page.waitForSelector('#eye-left.eye.eye-closed', { timeout: 150 })
// check the background color has changed
await eq.$('#eye-left.eye.eye-closed', {
style: { backgroundColor: 'black' },
})
// check that the text of the button changed to 'open'
await eq.$('button', { textContent: 'Click to open the left eye' })
})
tests.push(async ({ eq, page }) => {
// click the button a second time to open the left eye
const button = await page.$('button')
button.click()
// check that the class has been removed
await page.waitForSelector('#eye-left.eye:not(.eye-closed)', { timeout: 150 })
// check the background color has changed
await eq.$('#eye-left.eye:not(.eye-closed)', {
style: { backgroundColor: 'red' },
})
// check that the text of the button changed to 'close'
await eq.$('button', { textContent: 'Click to close the left eye' })
})

43
js/tests/bring-it-to-life-dom_test.mjs

@ -0,0 +1,43 @@
export const tests = []
tests.push(async ({ eq, page }) => {
// check the JS script has been linked
await eq.$('script', { type: 'module' })
// check the JS script has a valid src
const source = await page.$eval(
'script',
(node) => node.src.includes('.js') && node.src,
)
if (!source.length) throw Error('missing script src')
})
tests.push(async ({ eq, page }) => {
// check the class 'eye-closed' has been added in the CSS
eq.css('.eye-closed', {
height: '4px',
padding: '0px 5px',
borderRadius: '10px',
})
})
tests.push(async ({ eq, page }) => {
// check the class of left eye before the JS is loaded
await page.setJavaScriptEnabled(false)
await page.reload()
await eq.$('p#eye-left', { className: 'eye' })
})
tests.push(async ({ eq, page }) => {
// check the class of left eye has been updated after the JS is loaded
await page.setJavaScriptEnabled(true)
await page.reload()
await eq.$('p#eye-left', { className: 'eye eye-closed' })
// check the background color of left eye has changed after the JS is loaded
const eyeLeftBg = await page.$eval(
'#eye-left',
(node) => node.style.backgroundColor,
)
eq(eyeLeftBg, 'black')
})

109
js/tests/build-brick-and-break-dom_test.mjs

@ -0,0 +1,109 @@
export const tests = []
export const setup = async ({ page }) => ({
getBricksIds: async () =>
await page.$$eval('div', (nodes) =>
nodes.filter((node) => node.id.includes('brick')).map((n) => n.id),
),
})
const between = (expected, min, max) => expected >= min && expected <= max
tests.push(async ({ page, eq }) => {
// check that the brick divs are built at a regular interval of 100ms
// the average of the divs built every 100ms must be close to 10
let repeat = 0
let buildSteps = []
while (repeat < 3) {
const divs = await page.$$eval('div', (nodes) => nodes.length)
console.log(`step ${repeat + 1}: ${divs} bricks`)
buildSteps.push(divs)
await page.waitForTimeout(1000)
repeat++
}
const diff1 = buildSteps[1] - buildSteps[0]
const diff2 = buildSteps[2] - buildSteps[1]
const average = Math.round((diff1 + diff2) / 2)
if (average < 9) {
console.log('average too low --> new bricks built / sec:', average)
} else if (average > 11) {
console.log('average too high --> new bricks built / sec:', average)
} else {
console.log('good average of new bricks built / sec')
}
eq(between(average, 9, 11), between(10, 9, 11))
})
const allBricksIds = [...Array(54).keys()].map((i) => `brick-${i + 1}`)
tests.push(async ({ page, eq, ctx }) => {
// check that all the bricks are here and have the correct id
await page.waitForTimeout(3000)
const bricksIds = await ctx.getBricksIds()
eq(bricksIds, allBricksIds)
})
tests.push(async ({ page, eq }) => {
// check that the middle column bricks have the `foundation` attribute to `true`
const expectedIds = allBricksIds.filter(
(b) => b.replace('brick-', '') % 3 === 2,
)
const middleBricksIds = await page.$$eval('div', (nodes) =>
nodes
.filter((node) => node.id.includes('brick') && node.dataset.foundation)
.map((n) => n.id),
)
eq(middleBricksIds, expectedIds)
})
tests.push(async ({ page, eq }) => {
// check that the bricks to repair have the right repaired attribute
const hammer = await page.$('#hammer')
await hammer.click()
const expectedRepairedIds = await page.$eval('body', (body) => {
const getIdInt = (str) => str.replace('brick-', '')
return body.dataset.reparations
.split(',')
.sort((a, b) => getIdInt(b) - getIdInt(a))
.map((id) => {
const isMiddleBrick = getIdInt(id) % 3 === 2
const status = isMiddleBrick ? 'in progress' : 'repaired'
return `${id}_${status}`
})
})
const repairedIds = await page.$$eval('div', (nodes) => {
const getIdInt = (str) => str.replace('brick-', '')
return nodes
.filter(
(node) =>
node.dataset.repaired === 'true' ||
node.dataset.repaired === 'in progress',
)
.sort((a, b) => getIdInt(b.id) - getIdInt(a.id))
.map(({ id }) => {
const isMiddleBrick = getIdInt(id) % 3 === 2
const status = isMiddleBrick ? 'in progress' : 'repaired'
return `${id}_${status}`
})
})
eq(repairedIds, expectedRepairedIds)
})
tests.push(async ({ page, eq, getBricksIds }) => {
// check that the last brick is removed on each dynamite click
const dynamite = await page.$('#dynamite')
for (const i of allBricksIds.keys()) {
await dynamite.click()
const { length } = allBricksIds
const expectedRemainingBricks = allBricksIds.slice(0, length - (i + 1))
eq(await getBricksIds(), expectedRemainingBricks)
}
})

36
js/tests/class-that-dom_test.mjs

@ -0,0 +1,36 @@
export const tests = []
tests.push(async ({ page, eq }) => {
// check the class 'eye' has been declared properly in the CSS
eq.css('.eye', {
width: '60px',
height: '60px',
backgroundColor: 'red',
borderRadius: '50%',
})
})
tests.push(async ({ page, eq }) => {
// check the class 'arm' has been declared properly in the CSS
eq.css('.arm', { backgroundColor: 'aquamarine' })
})
tests.push(async ({ page, eq }) => {
// check the class 'leg' has been declared properly in the CSS
eq.css('.leg', { backgroundColor: 'dodgerblue' })
})
tests.push(async ({ page, eq }) => {
// check the class 'body-member' has been declared properly in the CSS
eq.css('.body-member', { width: '50px', margin: '30px' })
})
tests.push(async ({ page, eq }) => {
// check that the targetted elements have the correct class names
await eq.$('p#eye-left', { className: 'eye' })
await eq.$('p#eye-right', { className: 'eye' })
await eq.$('div#arm-left', { className: 'arm body-member' })
await eq.$('div#arm-right', { className: 'arm body-member' })
await eq.$('div#leg-left', { className: 'leg body-member' })
await eq.$('div#leg-right', { className: 'leg body-member' })
})

71
js/tests/fifty-shades-of-cold-dom_test.mjs

@ -0,0 +1,71 @@
import { colors } from '../subjects/fifty-shades-of-cold/fifty-shades-of-cold.data.js'
colors.sort()
const cold = ['aqua', 'blue', 'turquoise', 'green', 'purple', 'cyan', 'navy']
const isCold = color => cold.some(coldColor => color.includes(coldColor))
const toClass = (a, b) => `.${a} { background: ${b} }`
const toDiv = (a, b) => `<div class="${a}">${b}</div>`
export const tests = []
tests.push(async ({ page, eq }) => {
// check that the css is properly generated
const style = await page.$$eval('style', nodes => nodes[1].innerHTML)
const classes = style
.split('}')
.map(s => s.replace(/(\.|{|:|;|\s+)/g, ''))
.filter(Boolean)
.sort()
for (const [i, c] of colors.entries()) {
if (!classes[i]) throw Error(`Not enough class (expected: ${c})`)
const [a, b] = classes[i].split('background')
eq(toClass(a, b), toClass(c, c))
}
})
tests.push(async ({ page, eq }) => {
// check that the divs are properly generated
const values = await page.$$eval('div', nodes =>
nodes.map(n => [n.className, n.textContent]),
)
const divs = values.map(v => toDiv(...v)).sort()
let skipped = 0
for (const [i, c] of colors.entries()) {
if (isCold(c)) {
if (!values[i - skipped]) throw Error(`Not enough div (expected: ${c})`)
eq(divs[i - skipped], toDiv(c, c))
continue
}
skipped++
if (divs.includes(toDiv(c, c))) {
throw Error(`div ${toDiv(c, c)} is not very cold`)
}
}
})
tests.push(async ({ page, eq }) => {
// test that clicking update the color accordingly
const coldColors = colors.filter(isCold)
for (const c of coldColors) {
await page.$$eval(
'div',
(nodes, c) => nodes.find(n => n.textContent === c).click(),
c,
)
const count = await page.$$eval(
'div',
(nodes, c) => nodes.filter(n => n.className === c).length,
c,
)
eq(count, coldColors.length)
}
})

46
js/tests/first-words-dom_test.mjs

@ -0,0 +1,46 @@
export const tests = []
tests.push(async ({ eq, page }) => {
// check the class words has been added in the CSS
await eq.css('.words', { textAlign: 'center', fontFamily: 'sans-serif' })
})
tests.push(async ({ eq, page }) => {
// check the torso element is initially empty
const isEmpty = await page.$eval('#torso', (node) => !node.children.length)
eq(isEmpty, true)
})
tests.push(async ({ eq, page }) => {
// click on the button
const button = await page.$('button#speak-button')
await button.click()
// check a new text element is added in the torso
const torsoChildren = await page.$eval('#torso', (node) =>
[...node.children].map((child) => ({
tag: child.tagName,
text: child.textContent,
class: child.className,
})),
)
eq(torsoChildren, [textNode])
})
tests.push(async ({ eq, page }) => {
// click a second time on the button
const button = await page.$('button#speak-button')
await button.click()
// check a second new text element is added in the torso
const torsoChildren = await page.$eval('#torso', (node) =>
[...node.children].map((child) => ({
tag: child.tagName,
text: child.textContent,
class: child.className,
})),
)
eq(torsoChildren, [textNode, textNode])
})
const textNode = { tag: 'DIV', text: 'Hello there!', class: 'words' }

127
js/tests/get-them-all-dom_test.mjs

@ -0,0 +1,127 @@
import { people } from './subjects/get-them-all/get-them-all.data.js'
const getIds = predicate =>
people
.filter(predicate)
.map(e => e.id)
.sort((a, b) => a.localeCompare(b))
const architects = getIds(p => p.tag === 'a')
const notArchitects = getIds(p => p.tag !== 'a')
const classical = getIds(p => p.classe === 'classical')
const notClassical = getIds(p => p.tag === 'a' && p.classe !== 'classical')
const active = getIds(p => p.classe === 'classical' && p.active)
const notActive = getIds(
p => p.tag === 'a' && p.classe === 'classical' && p.active === false,
)
const bonanno = people.find(p => p.id === 'BonannoPisano').id
const notBonanno = getIds(
p =>
p.tag === 'a' &&
p.classe === 'classical' &&
p.active &&
p.id !== 'BonannoPisano',
)
export const tests = []
tests.push(async ({ eq, page }) => {
// get architects
const btnArchitect = await page.$(`#btnArchitect`)
btnArchitect.click()
await page.waitForTimeout(150)
const selected = await page.$$eval('a', nodes =>
nodes
.filter(node => node.textContent === 'Architect')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(selected, architects)
const eliminated = await page.$$eval('span', nodes =>
nodes
.filter(node => node.style.opacity === '0.2')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(eliminated, notArchitects)
})
tests.push(async ({ page, eq }) => {
// get classical
const btnClassical = await page.$(`#btnClassical`)
btnClassical.click()
await page.waitForTimeout(150)
const selected = await page.$$eval('.classical', nodes =>
nodes
.filter(node => node.textContent === 'Classical')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(selected, classical)
const eliminated = await page.$$eval('a:not(.classical)', nodes =>
nodes
.filter(node => node.style.opacity === '0.2')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(eliminated, notClassical)
})
tests.push(async ({ page, eq }) => {
// check active
const btnActive = await page.$(`#btnActive`)
btnActive.click()
await page.waitForTimeout(150)
const selected = await page.$$eval('.classical.active', nodes =>
nodes
.filter(node => node.textContent === 'Active')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(selected, active)
const eliminated = await page.$$eval('.classical:not(.active)', nodes =>
nodes
.filter(node => node.style.opacity === '0.2')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(eliminated, notActive)
})
tests.push(async ({ page, eq }) => {
// get bonanno
const btnBonanno = await page.$(`#btnBonanno`)
btnBonanno.click()
await page.waitForTimeout(150)
const selected = await page.$eval('#BonannoPisano', node => {
if (node.textContent === 'Bonanno Pisano') return node.id
})
eq(`bonanno: ${selected}`, `bonanno: ${bonanno}`)
const eliminated = await page.$$eval(
'a.classical.active:not(#BonannoPisano)',
nodes =>
nodes
.filter(node => node.style.opacity === '0.2')
.map(node => node.id)
.sort((a, b) => a.localeCompare(b)),
)
eq(eliminated, notBonanno)
})

123
js/tests/gossip-grid-dom_test.mjs

@ -0,0 +1,123 @@
import { gossips } from './subjects/gossip-grid/gossip-grid.data.js'
export const tests = []
tests.push(async ({ page, eq }) => {
// test that the grid is properly generated
const content = await page.$$eval('.gossip', nodes => nodes.map(n => n.textContent))
eq(content, ['Share gossip!', ...gossips])
})
tests.push(async ({ page, eq }) => {
// test that you can add a gossip
const rand = Math.random().toString(36).slice(2)
await page.type('textarea', `coucou ${rand}`)
await page.click('.gossip button')
const content = await page.$eval('div.gossip', n => n.textContent)
eq(content, `coucou ${rand}`)
})
const getStyle = (nodes, key) => nodes.map(n => n.style[key])
tests.push(async ({ page, eq }) => {
// test that you can change the width to the min
const min = await page.$eval('#width', n => {
n.value = n.min
n.dispatchEvent(new Event('input'))
return Number(n.min)
})
eq(min, 200)
const values = await page.$$eval('div.gossip', getStyle, 'width')
eq(Array(gossips.length + 1).fill(`${min}px`), values)
})
tests.push(async ({ page, eq }) => {
// test that you can change the width to the max
const max = await page.$eval('#width', n => {
n.value = n.max
n.dispatchEvent(new Event('input'))
return Number(n.max)
})
eq(max, 800)
const values = await page.$$eval('div.gossip', getStyle, 'width')
eq(Array(gossips.length + 1).fill(`${max}px`), values)
})
tests.push(async ({ page, eq }) => {
// test that you can change the font-size to the min
const min = await page.$eval('#fontSize', n => {
n.value = n.min
n.dispatchEvent(new Event('input'))
return Number(n.min)
})
eq(min, 20)
const values = await page.$$eval('div.gossip', getStyle, 'fontSize')
eq(Array(gossips.length + 1).fill(`${min}px`), values)
})
tests.push(async ({ page, eq }) => {
// test that you can change the font-size to the max
const max = await page.$eval('#fontSize', n => {
n.value = n.max
n.dispatchEvent(new Event('input'))
return Number(n.max)
})
eq(max, 40)
const values = await page.$$eval('div.gossip', getStyle, 'fontSize')
eq(Array(gossips.length + 1).fill(`${max}px`), values)
})
const getBackground = (nodes, key) => nodes.map(n => n.style.background || n.style.backgroundColor)
tests.push(async ({ page, eq, rgbToHsl }) => {
// test that you can change the background to the darkest
const min = await page.$eval('#background', n => {
n.value = n.min
n.dispatchEvent(new Event('input'))
return Number(n.min)
})
eq(min, 20)
const values = await page.$$eval('div.gossip', getBackground)
const lightness = values.map(rgbToHsl).map(([h,s,l]) => l)
eq(Array(gossips.length + 1).fill(min), lightness)
})
tests.push(async ({ page, eq, rgbToHsl }) => {
// test that you can change the background to the darkest
const max = await page.$eval('#background', n => {
n.value = n.max
n.dispatchEvent(new Event('input'))
return Number(n.max)
})
eq(max, 75)
const values = await page.$$eval('div.gossip', getBackground)
const lightness = values.map(rgbToHsl).map(([h,s,l]) => Math.round(l))
eq(Array(gossips.length + 1).fill(max), lightness)
})

67
js/tests/harder-bigger-bolder-stronger-dom_test.mjs

@ -0,0 +1,67 @@
export const tests = []
export const setup = async ({ page }) => ({
content: await page.$$eval('div', nodes => nodes.map(n => ({
text: n.textContent,
size: Number((n.style.fontSize || '').slice(0, -2)),
weight: Number(n.style.fontWeight),
})))
})
tests.push(({ eq, ctx }) => {
// should contain 120 items
eq(ctx.content.length, 120)
})
tests.push(({ eq, ctx }) => {
// ctx.content should only be one letter long
eq(ctx.content.reduce((total, { text }) => total + text.length, 0), 120)
})
tests.push(({ eq, ctx }) => {
// we expect random to yield at least 10 different letters
eq(new Set(ctx.content).size > 10, true)
})
tests.push(({ eq, ctx }) => {
// only letters from 'A' to 'Z'
eq(ctx.content.every(({ text }) => text >= 'A' && text <= 'Z'), true)
})
tests.push(({ eq, ctx }) => {
// letter size should grow
// first should be 11
eq(ctx.content[0].size, 11)
// last should be 120
eq(ctx.content[119].size, 130)
// each letter should be bigger than the previous
let prev = 0
for (const { size } of ctx.content) {
if (prev >= size) {
throw Error('Letters should grow')
}
}
})
tests.push(({ eq, ctx }) => {
// letter weight should increase in thirds
const third = n => ({ weight }) => weight === n
// first third should be 300
eq(ctx.content[0].weight, 300)
eq(ctx.content[39].weight, 300)
eq(ctx.content.slice(0, 40).every(third(300)), true)
// second third should be 400
eq(ctx.content[40].weight, 400)
eq(ctx.content[79].weight, 400)
eq(ctx.content.slice(40, 80).every(third(400)), true)
// last third should be 600
eq(ctx.content[80].weight, 600)
eq(ctx.content[119].weight, 600)
eq(ctx.content.slice(80).every(third(600)), true)
})

23
js/tests/hello-there_test.js

@ -1,23 +0,0 @@
import { readFileSync as read } from 'fs'
// /*/ // ⚡
export const tests = [
({ eq, path }) => // code must use console.log
read(path, 'utf8').trim().includes('console.log'),
async ({ eq, code }) => {
// console.log must have been called with the right value
const log = console.log.bind(console)
const args = []
console.log = (..._args) => {
args.push(..._args)
log(..._args)
}
const b64 = Buffer.from(code).toString("base64")
const url = `data:text/javascript;base64,${b64}`
await import(url)
console.log = log
return eq(args.join(' ').trim(), 'Hello There')
},
]

55
js/tests/keycodes-symphony-dom_test.mjs

@ -0,0 +1,55 @@
export const tests = []
export const setup = async ({ page }) => ({
getNotes: async () =>
await page.$$eval('.note', (nodes) => {
return nodes.map((note) => note.textContent)
}),
})
const characters = `didyouhandlethekeydowneventcorrectly`
tests.push(async ({ page, eq, ctx }) => {
// check that a note is created and matches the right letter when a key is pressed
for (const [i, character] of characters.split('').entries()) {
await page.keyboard.down(character)
const typed = characters.slice(0, i + 1).split('')
eq(await ctx.getNotes(), typed)
}
})
tests.push(async ({ page, eq, ctx }) => {
// check that the last note is removed when Backspace key is pressed
let step = 1
while (step < 10) {
await page.keyboard.down('Backspace')
const typed = characters.slice(0, characters.length - step).split('')
eq(await ctx.getNotes(), typed)
step++
}
})
tests.push(async ({ page, eq, ctx }) => {
// check that all the notes are cleared when Escape key is pressed
await page.keyboard.down('Escape')
const cleared = (await ctx.getNotes()).length === 0
eq(await cleared, true)
})
tests.push(async ({ page, eq }) => {
// check that notes have different background colors
const test = 'abcdefghijklmnopqrstuvwxyz'
let step = 0
while (step < test.length) {
await page.keyboard.down(test[step])
step++
}
const getNotesBg = await page.$$eval('.note', (nodes) => {
return nodes.map((note) => note.style.backgroundColor)
})
const colors = [...new Set(getNotesBg)]
const allDifferent = colors.length === test.length
eq(allDifferent, true)
})

122
js/tests/mouse-trap-dom_test.mjs

@ -0,0 +1,122 @@
export const tests = []
export const setup = async ({ page }) => ({
getCirclesPos: () =>
page.$$eval('.circle', nodes => {
const circleRadius = 25
const formatPos = pos => Number(pos.replace('px', '')) + circleRadius
return nodes.map(node => [
formatPos(node.style.left),
formatPos(node.style.top),
])
}),
})
tests.push(async ({ page, eq, ctx }) => {
// check that a circle is created on click at the mouse position
const { width, height } = await page.evaluate(() => ({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
}))
const clicks = [...Array(10).keys()].map(e => [random(width), random(height)])
for (const [i, click] of clicks.entries()) {
const [posX, posY] = click
await page.mouse.click(posX, posY)
const currentCircle = (await ctx.getCirclesPos())[i]
eq(currentCircle, click)
}
})
tests.push(async ({ page, eq, ctx }) => {
// check that the last created circle moves along the mouse
let move = 0
while (move < 100) {
move++
const x = move
const y = move * 2
await page.mouse.move(x, y)
const circles = await ctx.getCirclesPos()
const currentCirclePos = circles[circles.length - 1]
eq(currentCirclePos, [x, y])
}
})
tests.push(async ({ page, eq, ctx }) => {
// check that a circle is trapped and purple when inside the box
const box = await page.$eval('.box', box => ({
top: box.getBoundingClientRect().top,
right: box.getBoundingClientRect().right,
left: box.getBoundingClientRect().left,
bottom: box.getBoundingClientRect().bottom,
}))
await page.mouse.click(200, 200)
let move = 200
let hasEntered = false
while (move < 500) {
const x = move + 50
const y = move
await page.mouse.move(x, y)
const circles = await ctx.getCirclesPos()
const currentCircle = circles[circles.length - 1]
const circleRadius = 25
const bg = await page.$$eval(
'.circle',
nodes => nodes[nodes.length - 1].style.background,
)
const insideX = x > box.left + circleRadius && x < box.right - circleRadius
const insideY = y > box.top + circleRadius && y < box.bottom - circleRadius
const isInside = insideX && insideY
// check that the background is set to the right color
if (isInside) {
hasEntered = true
eq(bg, 'var(--purple)')
} else {
eq(bg, hasEntered ? 'var(--purple)' : 'white')
}
// check that the mouse is trapped inside the box
if (hasEntered) {
if (insideY) {
eq(currentCircle[1], y)
} else {
const maxY =
currentCircle[1] === box.top + circleRadius + 1 ||
currentCircle[1] === box.top + circleRadius ||
currentCircle[1] === box.bottom - circleRadius ||
currentCircle[1] === box.bottom - circleRadius - 1
eq(maxY, true)
}
if (insideX) {
eq(currentCircle[0], x)
} else {
const maxX =
currentCircle[0] === box.left + circleRadius ||
currentCircle[0] === box.left + circleRadius + 1 ||
currentCircle[0] === box.right - circleRadius ||
currentCircle[0] === box.right - circleRadius - 1
eq(maxX, true)
}
}
move++
}
})
const random = (min, max) => {
if (!max) {
max = min
min = 0
}
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}

90
js/tests/nesting-organs-dom_test.mjs

@ -0,0 +1,90 @@
export const tests = []
tests.push(async ({ page, eq }) => {
// check that the HTML structure is correct & elements are nested properly
const elements = await page.$$eval('body', (nodes) => {
const toNode = (el) => {
const node = {}
node.tag = el.tagName.toLowerCase()
node.id = el.id
if (el.children.length) {
node.children = [...el.children].map(toNode)
}
return node
}
return [...nodes[0].children].map(toNode)
})
eq(expectedStructure, elements)
})
tests.push(async ({ page, eq }) => {
// check the section selector style has been updated properly
eq.css('section', {
display: 'flex',
justifyContent: 'center',
})
})
tests.push(async ({ page, eq }) => {
// check if the provided CSS has been correctly copy pasted
eq.css('div, p', {
border: '1px solid black',
padding: '10px',
margin: '0px',
borderRadius: '30px',
})
eq.css('#face', { alignItems: 'center' })
eq.css('#eyes', {
display: 'flex',
backgroundColor: 'yellow',
justifyContent: 'space-between',
alignItems: 'center',
borderRadius: '50px',
width: '200px',
})
eq.css('#torso', {
width: '200px',
backgroundColor: 'violet',
})
})
const expectedStructure = [
{
tag: 'section',
id: 'face',
children: [
{
tag: 'div',
id: 'eyes',
children: [
{ tag: 'p', id: 'eye-left' },
{ tag: 'p', id: 'eye-right' },
],
},
],
},
{
tag: 'section',
id: 'upper-body',
children: [
{ tag: 'div', id: 'arm-left' },
{ tag: 'div', id: 'torso' },
{ tag: 'div', id: 'arm-right' },
],
},
{
tag: 'section',
id: 'lower-body',
children: [
{ tag: 'div', id: 'leg-left' },
{ tag: 'div', id: 'leg-right' },
],
},
]

110
js/tests/pick-and-click-dom_test.mjs

@ -0,0 +1,110 @@
export const tests = []
const between = (expected, min, max) => expected >= min && expected <= max
export const setup = async ({ page, rgbToHsl }) => ({
bodyBgRgb: async () =>
rgbToHsl(await page.$eval('body', (body) => body.style.background)),
})
tests.push(async ({ page, eq }) => {
// check that the background color is changing on mouse move
// by simulating 20 moves, so there must be 20 different background colors
let move = 50
let bgs = []
while (move < 250) {
move += 10
const x = move
const y = move * 2
await page.mouse.move(x, y)
const bodyBg = await page.$eval('body', (body) => body.style.background)
const validColor = bodyBg.includes('rgb')
if (!validColor) continue
bgs.push(bodyBg)
}
const differentBgs = [...new Set(bgs)].length
eq(differentBgs, 20)
})
const near = (a, b) => a < b + 2.5 && a > b - 2.5
const numVal = (n) => n.textContent.replace(/[^0-9,]/g, '')
const generateCoords = (random) => [
[random(125, 500), random(125, 400)],
[random(125, 500), random(125, 400)],
[random(125, 500), random(125, 400)],
]
tests.push(async ({ page, eq, bodyBgRgb, random }) => {
// check that the hsl value displayed matches the current background color
for (const move of generateCoords(random)) {
await page.mouse.move(...move)
const a = await bodyBgRgb()
const b = (await page.$eval('.hsl', numVal)).split(',')
if (a.every((v, i) => near(v, Number(b[i])))) continue
throw Error(`hsl(${a.map(Math.round)}) to far from hsl(${b})`)
}
})
tests.push(async ({ page, eq, bodyBgRgb, random }) => {
// check that the hue value displayed matches the current background color
for (const move of generateCoords(random)) {
await page.mouse.move(...move)
const [h] = await bodyBgRgb()
const hue = await page.$eval('.hue', numVal)
if (!near(h, Number(hue))) {
console.log({ h, hue, c: near(h, Number(hue)) })
throw Error(`hue ${Math.round(h)} to far from ${hue}`)
}
}
})
tests.push(async ({ page, eq, bodyBgRgb, random }) => {
// check that the luminosity value displayed matches the current background color
for (const move of generateCoords(random)) {
await page.mouse.move(...move)
const [, , l] = await bodyBgRgb()
const lum = await page.$eval('.luminosity', numVal)
if (!near(l, Number(lum))) {
throw Error(`luminosity ${Math.round(l)} to far from ${lum}`)
}
}
})
tests.push(async ({ page, eq, bodyBgRgb, random }) => {
// check that the hsl value is copied in the clipboard on click
// Override readText if writeText is used due to a puppeteer bug
await page.evaluate(() => {
window.navigator.clipboard.writeText = async (text) => {
window.navigator.clipboard.readText = async () => text
}
})
for (const move of generateCoords(random)) {
await page.mouse.click(...move)
const clipboard = await page.evaluate(() =>
window.navigator.clipboard.readText()
)
const hslValue = await page.$eval('.hsl', (hsl) => hsl.textContent)
eq(hslValue, clipboard)
}
})
tests.push(async ({ page, eq, bodyBgRgb, random }) => {
// check that each svg axis is following the mouse
const [[mouseX, mouseY]] = generateCoords(random)
await page.mouse.move(mouseX, mouseY)
const axisX = await page.$eval('#axisX', (line) => [
line.getAttribute('x1'),
line.getAttribute('x2'),
])
const axisY = await page.$eval('#axisY', (line) => [
line.getAttribute('y1'),
line.getAttribute('y2'),
])
const checkAxisCoords = (coords) => Number([...new Set(coords)].join())
eq(checkAxisCoords(axisX), mouseX)
eq(checkAxisCoords(axisY), mouseY)
})

36
js/tests/pimp-my-style-dom_test.mjs

@ -0,0 +1,36 @@
import { styles } from './subjects/pimp-my-style/pimp-my-style.data.js'
export const tests = []
const formatClass = (limit, unpimp) =>
['button', ...styles.slice(0, limit), unpimp && 'unpimp'].filter(Boolean)
const max = styles.length - 1
export const setup = async ({ page }) => {
const btn = await page.$('.button')
return {
btn,
getClass: async () =>
(await (await btn.getProperty('className')).jsonValue()).split(' '),
}
}
tests.push(async ({ page, eq, btn, getClass }) => {
// pimp
for (const i of styles.keys()) {
console.log('pimp click', i + 1)
await btn.click()
eq(formatClass(i + 1, i === max), await getClass())
}
})
tests.push(async ({ page, eq, btn, getClass }) => {
// unpimp !
for (const i of styles.keys()) {
console.log('unpimp click', i + 1)
await btn.click()
eq(formatClass(max - i, i !== max), await getClass())
}
})

49
js/tests/select-and-style-dom_test.mjs

@ -0,0 +1,49 @@
export const tests = []
// this test is commented out because it will not work for editor mode
// tests.push(async ({ eq }) => {
// // check the CSS stylesheet is linked in the head tag
//
// await eq.$('head link', {
// rel: 'stylesheet',
// href: 'http://localhost:9898/select-and-style/select-and-style.css',
// })
// })
tests.push(async ({ eq }) => {
// check the universal selector has been declared properly
await eq.css('*', {
margin: '0px',
opacity: '0.85',
boxSizing: 'border-box',
})
})
tests.push(async ({ eq }) => {
// check that the body was styled
await eq.css('body', { height: '100vh' })
})
tests.push(async ({ eq }) => {
// check that sections elements are styled
await eq.css('section', {
padding: '20px',
width: '100%',
height: 'calc(33.3333%)',
})
})
tests.push(async ({ eq }) => {
// check that the individual sections are styled
await eq.css('#face', { backgroundColor: 'cyan' })
await eq.css('#upper-body', { backgroundColor: 'blueviolet' })
await eq.css('#lower-body', { backgroundColor: 'lightsalmon' })
})

25
js/tests/skeleton-dom_test.mjs

@ -0,0 +1,25 @@
export const tests = []
tests.push(async ({ page, eq }) => {
// check that the title tag is present & is set with some text
const title = await page.$eval('title', (node) => node.textContent)
if (!title.length) throw Error('missing title')
})
tests.push(async ({ page, eq }) => {
// check the face
return eq.$('section:nth-child(1)', { textContent: 'face' })
})
tests.push(async ({ page, eq }) => {
// check the upper-body
return eq.$('section:nth-child(2)', { textContent: 'upper-body' })
})
tests.push(async ({ page, eq }) => {
// check the lower-body, my favorite part
return eq.$('section:nth-child(3)', { textContent: 'lower-body' })
})

164
js/tests/test.mjs

@ -1,8 +1,10 @@
import puppeteer from 'puppeteer'
import { join as joinPath, dirname, extname } from 'path'
import { fileURLToPath } from 'url'
import { readFile, writeFile } from 'fs/promises'
import { deepStrictEqual } from 'assert'
import * as fs from 'fs'
const { readFile, writeFile } = fs.promises
import { fileURLToPath } from 'url'
import http from 'http'
import fs from 'fs'
global.window = global
global.fetch = url => {
@ -21,6 +23,12 @@ global.fetch = url => {
const wait = delay => new Promise(s => setTimeout(s, delay))
const fail = fn => { try { fn() } catch (err) { return true } }
const upperFirst = (str) => str[0].toUpperCase() + str.slice(1)
const randStr = (n = 7) => Math.random().toString(36).slice(2, n)
const between = (min, max) => {
max || (max = min, min = 0)
return Math.floor(Math.random() * (max - min) + min)
}
const props = [String,Array]
.flatMap(({ prototype }) =>
@ -38,9 +46,16 @@ const eq = (a, b) => {
}
const [solutionPath, name] = process.argv.slice(2)
const tools = { eq, fail, wait, randStr, between, upperFirst }
const cleanup = (exitCode = 0) => {
if (!tools.browser) process.exit(exitCode)
tools.server.close()
return tools.browser.close().finally(() => process.exit(exitCode))
}
const fatal = (...args) => {
console.error(...args)
process.exit(1)
return cleanup(1)
}
solutionPath || fatal('missing solution-path, usage:\nnode test solution-path exercise-name')
@ -75,7 +90,7 @@ const any = arr =>
f(firstError)
})
const testNode = async ({ name }) => {
const testNode = async () => {
const path = `${solutionPath}/${name}.mjs`
return {
path,
@ -84,7 +99,7 @@ const testNode = async ({ name }) => {
}
}
const runInlineTests = async ({ json, name }) => {
const runInlineTests = async ({ json }) => {
const restore = new Set()
const equal = deepStrictEqual
const saveArguments = (src, key) => {
@ -103,12 +118,11 @@ const runInlineTests = async ({ json, name }) => {
const logs = []
console.log = (...args) => logs.push(args)
const die = (...args) => {
logs.forEach((args) => console.info(...args))
console.error(...args)
process.exit(1)
logs.forEach((logArgs) => console.info(...logArgs))
return fatal(...args)
}
const solution = await loadAndSanitizeSolution(name)
const solution = await loadAndSanitizeSolution()
for (const { description, code } of JSON.parse(json)) {
logs.length = 0
const [provided, tests] = code.includes('// Your code')
@ -130,14 +144,15 @@ ${tests.trim()}`.trim()
console.info(`${description}:`, 'PASS')
} catch (err) {
console.info(`${description}:`, 'FAIL')
console.info(`\n======= Code ======= \n${fullCode}`)
console.info('\n======= Error ======')
die(' ->', err.message)
console.info(' ->', err.message, '\n')
console.info('\n======= Code =======')
die(fullCode)
}
}
}
const loadAndSanitizeSolution = async name => {
const loadAndSanitizeSolution = async () => {
const path = `${solutionPath}/${name}.js`
const rawCode = await read(path, "student solution")
@ -151,18 +166,15 @@ const loadAndSanitizeSolution = async name => {
const runTests = async ({ url, path, code }) => {
const { setup, tests } = await import(url).catch(err =>
fatal(`Unable to execute ${name} solution, error:\n${stackFmt(err, url)}`),
fatal(`Unable to execute ${name}, error:\n${stackFmt(err, url)}`),
)
const randStr = (n = 7) => Math.random().toString(36).slice(2, n)
const between = (min, max) => {
max || (max = min, min = 0)
return Math.floor(Math.random() * (max - min) + min)
}
const upperFirst = (str) => str[0].toUpperCase() + str.slice(1)
const tools = { eq, fail, wait, code, path, randStr, between, upperFirst }
Object.assign(tools, { code, path })
tools.ctx = (await (setup && setup(tools))) || {}
const isDOM = name.endsWith('-dom')
if (isDOM) {
Object.assign(tools, await prepareForDOM({ code }))
}
let timeout
for (const [i, t] of tests.entries()) {
try {
@ -172,19 +184,108 @@ const runTests = async ({ url, path, code }) => {
timeout = setTimeout(f, 60000, Error('Time limit reached (1min)'))
}),
])
if (!(await waitWithTimeout)) {
if (!(await waitWithTimeout) && !isDOM) {
throw Error('Test failed')
}
} catch (err) {
console.log(`test #${i+1} failed:\n${t.toString()}\n\nError:`)
fatal(stackFmt(err, url))
console.info(`test #${i+1} failed:\n${t.toString()}\n`)
return fatal(stackFmt(err, url))
} finally {
clearTimeout(timeout)
}
}
console.log(`${name} passed (${tests.length} tests)`)
cleanup(0)
console.info(`${name} passed (${tests.length} tests)`)
}
// add puppeteer tests as JS language:
const PORT = 9898
const config = {
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
// This will write shared memory files into /tmp instead of /dev/shm,
// because Docker’s default for /dev/shm is 64MB
'--disable-dev-shm-usage',
],
headless: !process.env.DEBUG_PUPPETTEER,
}
// LEGACY random, use between instead (only used by dom exercise, to be replaced)
const random = (min, max = min) => {
max === min && (min = 0)
min = Math.ceil(min)
return Math.floor(Math.random() * (Math.floor(max) - min + 1)) + min
}
const rgbToHsl = rgbStr => {
const [r, g, b] = rgbStr.slice(4, -1).split(',').map(Number)
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const l = (max + min) / ((0xff * 2) / 100)
if (max === min) return [0, 0, l]
const d = max - min
const s = (d / (l > 50 ? 0xff * 2 - max - min : max + min)) * 100
if (max === r) return [((g - b) / d + (g < b && 6)) * 60, s, l]
return max === g
? [((b - r) / d + 2) * 60, s, l]
: [((r - g) / d + 4) * 60, s, l]
}
const prepareForDOM = ({ code }, server) => new Promise((s, f) => (server = http
.createServer(({ url, method }, response) => {
console.info(method + ' ' + url)
// Loading either the `index.html` or the js code (student solution)
response.setHeader('Content-Type', 'text/html')
return response.end(`<script type="module">${code}</script>`)
}))
.listen(PORT, async listenErr => {
if (listenErr) return f(listenErr)
try {
const browser = await puppeteer.launch(config)
const [page] = await browser.pages()
await page.goto(`http://localhost:${PORT}/index.html`)
deepStrictEqual.$ = async (selector, props) => {
const keys = Object.keys(props)
const extractProps = (node, props) => {
const fromProps = (a, b) => Object.fromEntries(Object.keys(b).map(k => [
k,
typeof b[k] === 'object' ? fromProps(a[k], b[k]) : a[k],
]))
return fromProps(node, props)
}
const domProps = await page.$eval(selector, extractProps, props)
return deepStrictEqual(props, domProps)
}
deepStrictEqual.css = async (selector, props) => {
const cssProps = await page.evaluate((selector, props) => {
const styles = Object.fromEntries([...document.styleSheets]
.flatMap(({ cssRules }) => [...cssRules].map(r => [r.selectorText, r.style])))
if (!styles[selector]) {
throw Error(`css ${selector} did not match any declarations`)
}
return Object.fromEntries(Object.keys(props).map(k => [k, styles[selector][k]]))
}, selector, props)
return deepStrictEqual(props, cssProps)
}
browser
.defaultBrowserContext()
.overridePermissions(`http://localhost:${PORT}`, ['clipboard-read'])
s({ page, browser, random, rgbToHsl, eq: deepStrictEqual, server })
} catch (err) {
f(err)
}
}))
const main = async () => {
const { test, mode } = await any([
readTest(joinPath(root, `${name}.json`)),
@ -192,19 +293,22 @@ const main = async () => {
readTest(joinPath(root, `${name}_test.mjs`)),
]).catch(ifNoEnt((err) => fatal(`Missing test for ${name}`)))
if (mode === "node") return runTests(await testNode({ test, name }))
if (mode === "inline") return runInlineTests({ json: test, name })
if (mode === "node") return runTests(await testNode())
if (mode === "inline") return runInlineTests({ json: test })
const { rawCode, code, path } = await loadAndSanitizeSolution(name)
const { rawCode, code, path } = await loadAndSanitizeSolution()
const parts = test.split("// /*/ // ⚡")
const [inject, testCode] = parts.length < 2 ? ["", test] : parts
const combined = `${inject.trim()}\n${rawCode
.replace(inject.trim(), "")
.trim()}\n${testCode.trim()}\n`
// write to file and read file instead ?
const b64 = Buffer.from(combined).toString("base64")
const url = `data:text/javascript;base64,${b64}`
return runTests({ path, code, url })
}
main().catch(err => fatal(err.stack))
main().catch(err => {
fatal(err?.stack || Error('').stack)
})

19
js/tests/the-calling-dom_test.mjs

@ -0,0 +1,19 @@
export const tests = []
tests.push(async ({ page, eq }) => {
// check the face
return eq.$('section#face', { textContent: '' })
})
tests.push(async ({ page, eq }) => {
// check the upper-body
return eq.$('section#upper-body', { textContent: '' })
})
tests.push(async ({ page, eq }) => {
// check the lower-body, my favorite part
return eq.$('section#lower-body', { textContent: '' })
})

174
js/tests/where-do-we-go-dom_test.mjs

@ -0,0 +1,174 @@
import { places } from './subjects/where-do-we-go/where-do-we-go.data.js'
export const tests = []
const random = (min, max) => {
if (!max) {
max = min
min = 0
}
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
const getDegree = coordinates => {
const north = coordinates.includes('N')
const degree = coordinates.split("'")[0].replace('°', '.')
return north ? degree : -degree
}
export const setup = async ({ page }) => {
return {
getDirection: async () =>
await page.$eval('.direction', direction => direction.textContent),
}
}
const sortedPlaces = places.sort(
(a, b) => getDegree(b.coordinates) - getDegree(a.coordinates),
)
const dataNames = sortedPlaces.map(({ name }) =>
name
.split(',')[0]
.toLowerCase()
.split(' ')
.join('-'),
)
tests.push(async ({ page, eq }) => {
const { width, height } = await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}))
const sections = await page.$$eval('section', sections =>
sections.map(section => {
return [
section.getBoundingClientRect().width,
section.getBoundingClientRect().height,
]
}),
)
console.log(`Must contain ${places.length} places`)
// check that the correct amount of sections has been generated
eq(sections.length, places.length)
// check that all the sections are fullscreen-size
eq([...new Set(...sections)], [width, height])
})
tests.push(async ({ page, eq }) => {
// check that the sections have been generated with the correponding images as background,
// and sorted in the right order (from the Northest to the Southest)
const imageNames = await page.$$eval('section', sections =>
sections.map(section => {
const test = section.style.background.split('.jpg')[0].split('/')
return test[test.length - 1]
}),
)
console.log(`Must be sorted from North to South`)
console.log(`Must have the right images in background`)
eq(imageNames, dataNames)
})
tests.push(async ({ page, eq }) => {
// check that the location indicator is updating according to the image displayed
let step = 1
while (step < 6) {
await page.evaluate(() => {
window.scrollBy(0, window.innerHeight + 200)
})
await page.waitForTimeout(150)
const location = await page.$eval('.location', location => [
...location.textContent.split('\n'),
location.style.color,
])
const currentLocationIndex = await page.evaluate(() =>
Math.round(window.scrollY / window.innerHeight),
)
const currentLocation = sortedPlaces[currentLocationIndex]
const { name, coordinates, color } = currentLocation
const expectedLocation = [name, coordinates, color]
// check that the location indicator and the displayed location contents are matching
console.log(`Scroll #${step}: displaying ${location[0]}`)
eq(location, expectedLocation)
step++
}
})
tests.push(async ({ page, eq, getDirection }) => {
// check that the compass is pointing 'S' when scrolling down
await page.evaluate(() => {
window.scrollBy(0, window.innerHeight)
})
await page.waitForTimeout(100)
const direction = (await getDirection()).includes('S')
? 'S'
: await getDirection()
console.log('Scroll down: pointing', direction)
eq(direction, 'S')
})
tests.push(async ({ page, eq, getDirection }) => {
// check that the compass is pointing 'N' when scrolling up
await page.evaluate(() => {
window.scrollBy(0, -100)
})
await page.waitForTimeout(100)
const direction = (await getDirection()).includes('N')
? 'N'
: await getDirection()
console.log('Scroll up: pointing', direction)
eq(direction, 'N')
})
tests.push(async ({ page, eq }) => {
// check that the location target attribute is set to '_blank' to open a new tab
const locationTarget = await page.$eval('.location', ({ target }) => target)
console.log(
`Location <a> tag target attribute ${
locationTarget === '_blank' ? '' : 'not '
}set to open a new tab`,
)
eq(locationTarget, '_blank')
})
tests.push(async ({ page, eq }) => {
// check that the location href is valid
const location = await page.$eval('.location', ({ href, textContent }) => ({
href,
textContent,
}))
const isValidUrl = location.href.includes('google.com/maps')
const coords = location.textContent.split('\n')[1]
const convertedUrl = location.href
.split('%C2%B0')
.join('°')
.split('%22')
.join('"')
.split('%20')
.join(' ')
const isValidCoordinates = convertedUrl.includes(coords)
console.log('URL', location.href, isValidUrl ? 'valid' : 'invalid')
eq(isValidUrl, true)
console.log(
`Matching coordinates ${coords} ${
isValidCoordinates ? '' : 'not '
}found in URL`,
)
eq(isValidCoordinates, true)
})

81
subjects/action-reaction-dom/README.md

@ -0,0 +1,81 @@
## Action - reaction!
### Resources
We provide you with some content to get started smoothly, check it out!
- Video [querySelector](https://www.youtube.com/watch?v=m34qd7aGMBo&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=12)
- Video [DOM JS - Add an event listener to an element](https://www.youtube.com/watch?v=ydRv338Fl8Y)
- Video [DOM JS - Set an element's properties](https://www.youtube.com/watch?v=4O6zSVR0ufw&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=14)
- Video [DOM JS - classList: toggle, replace & contains](https://www.youtube.com/watch?v=amEBcoTYw0s&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=20)
- Video [DOM JS - Set an element's inline style](https://www.youtube.com/watch?v=pxlYKvju1z8&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=15)
- [Memo DOM JS](https://github.com/nan-academy/js-training/blob/gh-pages/examples/dom.js)
### Instructions
OK, you have now connected HTML, CSS and JS altogether ; big day! Excited? Exhausted?
Well so far, you've only scratched the surface... Let's go deeper into the power of JS! You're going to add some interaction ; the webpage will react when a user action will happen, called an [event](https://developer.mozilla.org/en-US/docs/Web/Events) (a click, a key pressed, a mouse move, etc.).
Let's put a button on the top right corner of the page, that will toggle (close or open) the left eye when clicked.
Add it in the HTML structure:
```html
<button>Click to close the left eye</button>
```
Add this CSS style:
```css
button {
z-index: 1;
position: fixed;
top: 30px;
right: 30px;
padding: 20px;
}
```
In the JS file, get the HTML button element with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector), and [add an event listener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) on [`click` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#javascript), triggering a function that will:
- change the [text content](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) of the button: if the eye is open, write "Click to close the left eye", if the eye is closed, write "Click to open the left eye"
- [toggle](https://css-tricks.com/snippets/javascript/the-classlist-api/) the class `eye-closed` in the `classList` of the `eye-left` HTML element
- change the background color of the `eye-left`: if the eye is open, to "red", if the eye is closed, to "black"
### Code examples
Add an event listener on click on a button that triggers a function:
```js
// events allow you to react to user inputs
// (any action with the mouse, keyboard, etc.)
// it's the foundation of the interactivity of your website
// each event is linked to an element or the window
// for this example we will attach a click event to a button
// first we select the button HTML element
const button = document.querySelector('button')
// we need to create a function
// that will be executed when the event is triggered
// let's call it `handleClick`
const handleClick = (event) => {
// do semething when the button has been clicked
}
// register the event:
button.addEventListener('click', handleClick)
// here we ask the button to call our `handleClick` function
// on the 'click' event, so every time it's clicked
```
### Expected output
[This](https://youtu.be/Wkar5SmswDo) is what you should see in the browser.
### Notions
- [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- [Text content of a HTML element](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
- [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) / [`click` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#javascript)
- [`classList` / `toggle`](https://css-tricks.com/snippets/javascript/the-classlist-api/)
- [Setting style with JS](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style#setting_styles)

50
subjects/bring-it-to-life-dom/README.md

@ -0,0 +1,50 @@
## Bring it to life
### Resources
We provide you with some content to get started smoothly, check it out!
- Video [DOM JS - getElementById](https://www.youtube.com/watch?v=34kAR8yBtDM&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=8)
- Video [DOM JS - Set an element's inline style](https://www.youtube.com/watch?v=pxlYKvju1z8&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=15)
- Video [DOM JS - classList: add & remove](https://www.youtube.com/watch?v=uQEM-3_4vPA&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=17)
- [Memo DOM JS](https://github.com/nan-academy/js-training/blob/gh-pages/examples/dom.js)
### Instructions
Still there? Well done! But hold on, here begins the serious part... In order to control your creation, you're going to plug its brain: JavaScript.
You're going to close the left eye of your entity.
To do so, you have to target the `eye-left` HTML element by its `id` thanks to the [`getElementById`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById) method. Then, [set the `style`](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style#setting_styles) of your `eye-left` to set its background color to "black". We also need to modify its shape ; for that we are going to add a new class to it.
First, define this new class in your style block:
```
.eye-closed {
height: 4px;
padding: 0 5px;
border-radius: 10px;
}
```
In the JS code, add the freshly-created class `eye-closed` to the [`classList`](https://css-tricks.com/snippets/javascript/the-classlist-api/) of your `eye-left` element.
Reload the page - it's alive! Your JS brain has control and orders your HTML/CSS body to close one eye.
### Code examples
Get a HTML element by its `id` & set its inline style:
```js
const myDiv = document.getElementById('my-div')
myDiv.style.color = 'green'
```
### Expected output
This is what you should see in the browser:
![](https://github.com/01-edu/public/raw/master/subjects/bring-it-to-life/bring-it-to-life.png)
### Notions
- [`getElementById`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById)
- [`classList` / `add()`](https://css-tricks.com/snippets/javascript/the-classlist-api/)
- [Setting style with JS](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style#setting_styles)

BIN
subjects/bring-it-to-life-dom/bring-it-to-life.png

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 48 KiB

44
subjects/build-brick-and-break-dom/README.md

@ -0,0 +1,44 @@
## Build brick and break
### Instructions
Today, your mission is to build a 3-column brick tower, maintain it and finally break it!
- Create a function `build` which will create and display the given amount of bricks passed as argument:
- each brick has to be created as a `div` and added to the page at a regular interval of 100ms,
- each brick will receive a unique `id` property, like following:
```html
<div id="brick-1"></div>
```
- each brick in the middle column has to be set with the custom data attribute `foundation` receiving the value `true`
- Each one of the two emojis in the top-right corner fires a function on click:
- 🔨 triggers the function `repair`: write the body of that function, which receives any number of `ids`, and for each `id`, retrieves the HTML element and set a custom attribute `repaired` set to `in progress` if it is a brick situated in the middle column, and `true` if not
- 🧨 triggers the function `destroy`: write the body of that function, which removes the current last brick in the tower
### Notions
- [`createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)
- [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append)
- [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
- [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) / [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval)
- [`hasAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)
- [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset) / [`data-*`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*)
- [`remove()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove)
### Files
You only need to create & submit the JS file `build-brick-and-break.js` ; we're providing you the following file to download (click right and save link) & test locally:
- the HTML file [build-brick-and-break.html](./build-brick-and-break.html) to open in the browser, which includes:
- the JS script running some code, and which will also allow to run yours
- some CSS pre-styled classes: feel free to use those as they are, or modify them
### Expected result
You can see an example of the expected result [here](https://youtu.be/OjSP_7u9CZ4)

108
subjects/build-brick-and-break-dom/build-brick-and-break.html

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<title>Build brick and break</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style type="text/css">
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
align-content: flex-end;
height: 100vh;
color: var(--text);
padding: 10vh 36.5vw;
}
div {
text-align: center;
font-size: 10px;
width: 9vw;
display: inline-flex;
justify-content: center;
align-items: center;
height: 4.44vh;
background: linear-gradient(-25deg, var(--clear) 30%, var(--disabled) 90%);
}
#tools {
position: fixed;
right: 100px;
font-size: 80px;
cursor: pointer;
user-select: none;
}
[data-repaired='true'] {
color: hsl(275, 100%, 50%);
}
[data-repaired='true']:after {
content: '-repaired';
}
[data-repaired='in progress'] {
color: black;
}
[data-repaired='in progress']:after {
content: '-in progress';
}
</style>
</head>
<body>
<script type="module">
import { build, repair, destroy } from './build-brick-and-break.js'
build(54)
const body = document.querySelector('body')
const tools = document.createElement('section')
tools.id = 'tools'
body.append(tools)
const dynamite = document.createElement('span')
dynamite.id = 'dynamite'
dynamite.textContent = '🧨'
dynamite.addEventListener('click', destroy)
const hammer = document.createElement('span')
hammer.id = 'hammer'
hammer.textContent = '🔨'
hammer.addEventListener('click', () => repair(...reparations))
tools.append(dynamite, hammer)
const random = (min, max) => {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
const randomBricks = [...Array(15).keys()].map((e) => `brick-${random(1, 54)}`)
const reparations = [...new Set(['brick-26', ...randomBricks])]
body.dataset.reparations = reparations
</script>
</body>
</html>

57
subjects/class-that-dom/README.md

@ -0,0 +1,57 @@
## Class that!
### Resources
We provide you with some content to get started smoothly, check it out!
- Video [CSS - Set & style with CSS class](https://www.youtube.com/watch?v=-U397k4VloU&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=6)
### Instructions
Alright, your being is almost done, some elements still need a bit more shaping and then we'll make it come to life!
If you look at your page, you can observe that some elements come by pair: the eyes, the arms & the legs. It is the same organ, one on the left and one on the right ; they have exactly the same shape, so for practicity & to avoid to repeat twice the same style, we're not going to use their `id` to style them, but a [`class`](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) ; contrary to an `id`, a `class` can be attributed to several different elements with common rulesets, and so the style defined for that class will apply to all the HTML elements that have it.
Create the 3 following classes, setting them with the given rulesets, & attribute them to the corresponding HTML elements:
- class `eye`:
- `width` of 60 pixels
- `height` of 60 pixels
- `background-color` "red"
- `border-radius` of 50%
- attributed to `eye-left` & `eye-right`
- class `arm`:
- `background-color` "aquamarine"
- attributed to `arm-left` & `arm-right`
- class `leg`:
- `background-color` "dodgerblue"
- attributed to `leg-left` & `leg-right`
Note that you can attribute several classes to a same element ; create the class `body-member`, which set the `width` to 50 pixels and the `margin` to 30 pixels, and add it to the `class` attribute of those elements: `arm-left`, `arm-right`, `leg-left` & `leg-right`.
### Code examples
Declare a class `my-first-class` and style it with a `color` to `"blue"` and a `background-color` to `"pink"`:
```css
.my-first-class {
color: blue;
background-color: pink;
}
```
Apply classes to HTML elements:
```html
<div class="my-first-class"></div>
<div class="another-class"></div>
<div class="my-first-class another-class"></div>
```
### Expected output
This is what you should see in the browser:
![](https://github.com/01-edu/public/raw/master/subjects/class-that/class-that.png)
### Notions
- [CSS class](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors)

BIN
subjects/class-that-dom/class-that.png

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 49 KiB

46
subjects/fifty-shades-of-cold-dom/README.md

@ -0,0 +1,46 @@
## Fifty shades of cold
### Instructions
You've been asked to freshen a webpage atmosphere by displaying shades of cold colors.
Check the `colors` array provided in the data file below.
- Write the `generateClasses` function which creates a `<style>` tag in the `<head>` tag and generates, for each color of `colors`, a class setting the `background` attribute and taking the color as value, like following:
```css
.blue {
background: blue;
}
```
- Write the `generateColdShades` function which creates a `<div>` for each color of the `colors` array whose name contains `aqua`, `blue`, `turquoise`, `green`, `cyan`, `navy` or `purple`.\
Each `<div>` must have the corresponding generated class and display the name of the color, like following:
```html
<div class="blue">blue</div>
```
- The function `choseShade` is triggered when clicking on a `div`.\
Write the body of this function, which receives the shade of the clicked element as argument, and replaces all the other elements class by the chosen shade.
### Notions
- [`head`](https://developer.mozilla.org/en-US/docs/Web/API/Document/head) / [style tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style)
- [`className`](https://developer.mozilla.org/en-US/docs/Web/API/Element/className)
- [`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList): `contains()`, `replace()`
### Files
You only need to create & submit the JS file `fifty-shades-of-cold.js` ; we're providing you the following files to download (click right and save link) & test locally:
- the HTML file [fifty-shades-of-cold.html](./fifty-shades-of-cold.html) to open in the browser, which includes:
- the JS script running some code, and which will also allow to run yours
- some CSS pre-styled classes: feel free to use those as they are, or modify them
- the data file [fifty-shades-of-cold.data.js](./fifty-shades-of-cold.data.js) from which you can import `colors`
### Expected result
You can see an example of the expected result [here](https://youtu.be/a-3JDEvW-Qg)

143
subjects/fifty-shades-of-cold-dom/fifty-shades-of-cold.data.js

@ -0,0 +1,143 @@
export const colors = [
'indianred',
'lightcoral',
'salmon',
'darksalmon',
'lightsalmon',
'crimson',
'red',
'firebrick',
'darkred',
'pink',
'lightpink',
'hotpink',
'deeppink',
'mediumvioletred',
'palevioletred',
'orange',
'coral',
'tomato',
'orangered',
'darkorange',
'yellow',
'gold',
'lightyellow',
'lemonchiffon',
'lightgoldenrodyellow',
'papayawhip',
'moccasin',
'peachpuff',
'palegoldenrod',
'khaki',
'darkkhaki',
'lavender',
'thistle',
'plum',
'violet',
'orchid',
'fuchsia',
'magenta',
'mediumorchid',
'mediumpurple',
'rebeccapurple',
'blueviolet',
'darkviolet',
'darkorchid',
'darkmagenta',
'purple',
'indigo',
'slateblue',
'darkslateblue',
'green',
'greenyellow',
'chartreuse',
'lawngreen',
'lime',
'limegreen',
'palegreen',
'lightgreen',
'mediumspringgreen',
'springgreen',
'mediumseagreen',
'seagreen',
'forestgreen',
'darkgreen',
'yellowgreen',
'olivedrab',
'olive',
'darkolivegreen',
'mediumaquamarine',
'darkseagreen',
'lightseagreen',
'darkcyan',
'teal',
'aqua',
'cyan',
'lightcyan',
'paleturquoise',
'aquamarine',
'turquoise',
'mediumturquoise',
'darkturquoise',
'cadetblue',
'steelblue',
'lightsteelblue',
'powderblue',
'lightblue',
'skyblue',
'lightskyblue',
'deepskyblue',
'dodgerblue',
'cornflowerblue',
'mediumslateblue',
'royalblue',
'blue',
'mediumblue',
'darkblue',
'navy',
'midnightblue',
'brown',
'cornsilk',
'blanchedalmond',
'bisque',
'navajowhite',
'wheat',
'burlywood',
'tan',
'rosybrown',
'sandybrown',
'goldenrod',
'darkgoldenrod',
'peru',
'chocolate',
'saddlebrown',
'sienna',
'maroon',
'white',
'snow',
'honeydew',
'mintcream',
'azure',
'aliceblue',
'ghostwhite',
'whitesmoke',
'seashell',
'beige',
'oldlace',
'floralwhite',
'ivory',
'antiquewhite',
'linen',
'lavenderblush',
'mistyrose',
'gainsboro',
'lightgray',
'silver',
'darkgray',
'gray',
'dimgray',
'lightslategray',
'slategray',
'darkslategray',
'black',
]

61
subjects/fifty-shades-of-cold-dom/fifty-shades-of-cold.html

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>Fifty shades of cold</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
width: 100vw;
padding: 5rem;
font-size: 12px;
}
div {
cursor: pointer;
width: 150px;
height: 150px;
margin: 20px;
padding: 5px 8px;
}
</style>
</head>
<body>
<script type="module">
import { generateClasses, generateColdShades, choseShade } from './fifty-shades-of-cold.js'
generateClasses()
generateColdShades()
const divs = [...document.querySelectorAll('div')]
divs.map((d) => {
d.addEventListener('click', () => choseShade(d.textContent))
})
</script>
</body>
</html>

72
subjects/first-words-dom/README.md

@ -0,0 +1,72 @@
## First words
### Resources
We provide you with some content to get started smoothly, check it out!
- Video [DOM JS - createElement & append](https://www.youtube.com/watch?v=J-A_pqTqGBU&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=13)
- Video [Set an element's className](https://www.youtube.com/watch?v=h3b7H1ZKvFE&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=16)
- Video [DOM JS - Set an element's properties](https://www.youtube.com/watch?v=4O6zSVR0ufw&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=14)
- Video [DOM JS - Add an event listener to an element](https://www.youtube.com/watch?v=ydRv338Fl8Y)
- [Memo DOM JS](https://github.com/nan-academy/js-training/blob/gh-pages/examples/dom.js)
### Instructions
Now that you know how to make your creation move, what about making it communicate its first words to the world?
Let's put a second button in the top right corner of the page, that will add some text when clicked.
Add it in the HTML structure:
```
<button id="speak-button">Click me to speak</button>
```
Add the button style in the CSS file:
```
button#speak-button {
top: 100px;
}
```
Also add this class to style the text we will add:
```
.words {
text-align: center;
font-family: sans-serif;
}
```
In the JS file, like in the previous exercise, get the HTML button element with `id` `speak-button` and add an event listener on `click` event, triggering a function that will:
- [create a new HTML element](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) of type `div`
- set its [text content](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) to "Hello there!"
- set its [`className`](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) to `words`, that we just added earlier in the CSS
- use the [`append`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append) method to add it inside the `torso` element
### Code examples
Create a new element and add it inside the body:
```js
// create a new `div` element
const div = document.createElement('div') // the argument passed (string) is the html tag
// select the `body` and add the new `div` inside it
const body = document.querySelector('body')
body.append(div)
```
### Expected output
[This](https://youtu.be/Eq9liRCc-zA) is what you should see in the browser.
### Notions
- [`getElementById`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById)
- [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) / [`click` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#javascript)
- [`createElement`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)
- [Text content of a HTML element](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
- [`className`](https://developer.mozilla.org/en-US/docs/Web/API/Element/className)
- [`append`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append)

53
subjects/get-them-all-dom/README.md

@ -0,0 +1,53 @@
## Get them all
### Instructions
You've been attributed the task to find the main architect of the Tower of Pisa before he achieves his plans, avoiding us nowadays all those lame pictures of people pretending to stop it from falling.
You arrive at the architects' chamber to find him, but all you have in front of you is a bunch of unknown people.
Step by step, with the little information you have, gather information and figure out by elimination who he is.
Launch the provided HTML file in the browser to begin your investigation.<br/>
On top of the webpage, each of the four buttons fires a function:
- Write the body of the `getArchitects` function, which returns an array containing 2 arrays of HTML elements:
- the first array contains the architects, all corresponding to a `<a>` tag
- the second array contains all the non-architects people
- Write the body of the `getClassical` function, which returns an array containing 2 arrays of HTML elements:
- the first array contains the architects belonging to the `classical` class
- the second array contains the non-classical architects
- Write the body of the `getActive` function, which returns an array containing 2 arrays of HTML elements:
- the first array contains the classical architects who are `active` in their class
- the second array contains the non-active classical architects
- Write the body of the `getBonannoPisano` function, which returns an array containing:
- the HTML element of the architect you're looking for, whose `id` is `BonannoPisano`
- an array which contains all the remaining HTML elements of active classical architects
> From now on, don't forget to [**export**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) all the expected functions, so that they can be imported to be tested<br/> > `export const getArchitects = () => {...}`
### Notions
- [HTML Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
- [`getElementsByTagName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByTagName)
- [`getElementsByClassName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName)
- [`getElementById()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById)
- [`querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) / [`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
- ...and bit of CSS that could help with the [`:not` pseudo class](https://developer.mozilla.org/en-US/docs/Web/CSS/:not)
### Files
You only need to create & submit the JS file `get-them-all.js` ; we're providing you the following files to download (click right and save link) & test locally:
- the HTML file [get-them-all.html](./get-them-all.html) to open in the browser, which includes:
- the JS script running some code, and which will also allow to run yours
- some CSS pre-styled classes: feel free to use those as they are, or modify them
- the import of the data
- the data file [get-them-all.data.js](./get-them-all.data.js) used to generate content in the HTML

47
subjects/get-them-all-dom/get-them-all.data.js

@ -0,0 +1,47 @@
export const people = [
{ id: 'LolaDunam', tag: 'span', classe: 'modern', active: false },
{ id: 'LeeMarley', tag: 'span', classe: 'baroque', active: false },
{ id: 'JeanDujardin', tag: 'a', classe: 'classical', active: true },
{ id: 'MarloStanfield', tag: 'span', classe: 'modern', active: false },
{ id: 'GeorgesDrumond', tag: 'span', classe: 'baroque', active: true },
{ id: 'JuliaWhite', tag: 'span', classe: 'modern', active: true },
{ id: 'BarneyLeberre', tag: 'span', classe: 'modern', active: true },
{ id: 'DavidCarretta', tag: 'a', classe: 'classical', active: false },
{ id: 'AugustoCesar', tag: 'span', classe: 'modern', active: true },
{ id: 'DavidGuetta', tag: 'a', classe: 'modern', active: false },
{ id: 'MarlonBrando', tag: 'a', classe: 'classical', active: false },
{ id: 'BonannoPisano', tag: 'a', classe: 'classical', active: true },
{ id: 'AvonBarksdale', tag: 'span', classe: 'baroque', active: true },
{ id: 'BarackObama', tag: 'span', classe: 'baroque', active: false },
{ id: 'MarcDupont', tag: 'span', classe: 'modern', active: false },
{ id: 'BillieElliott', tag: 'a', classe: 'baroque', active: true },
{ id: 'MariaCallas', tag: 'a', classe: 'baroque', active: false },
{ id: 'SteveJobbs', tag: 'a', classe: 'classical', active: false },
{ id: 'JoeLee', tag: 'span', classe: 'baroque', active: false },
{ id: 'AnthonyGrant', tag: 'span', classe: 'baroque', active: false },
{ id: 'ShakimaGreggs', tag: 'a', classe: 'modern', active: true },
{ id: 'RoyDeere', tag: 'span', classe: 'baroque', active: true },
{ id: 'BobTurner', tag: 'a', classe: 'classical', active: true },
{ id: 'AngeloCapri', tag: 'span', classe: 'modern', active: false },
{ id: 'SamMcDonald', tag: 'span', classe: 'baroque', active: true },
{ id: 'FannyLelouche', tag: 'span', classe: 'baroque', active: true },
{ id: 'ClarkLoister', tag: 'a', classe: 'classical', active: false },
{ id: 'FinanObrien', tag: 'span', classe: 'modern', active: false },
{ id: 'ClariceSterling', tag: 'a', classe: 'modern', active: true },
{ id: 'JayHernan', tag: 'span', classe: 'baroque', active: true },
{ id: 'HelenMirren', tag: 'a', classe: 'classical', active: false },
{ id: 'SarahForestier', tag: 'a', classe: 'modern', active: false },
{ id: 'JacquesChirac', tag: 'a', classe: 'classical', active: true },
{ id: 'MartinWealer', tag: 'a', classe: 'baroque', active: true },
{ id: 'JodieFoster', tag: 'span', classe: 'baroque', active: true },
{ id: 'JeanJacques', tag: 'span', classe: 'modern', active: false },
{ id: 'MollyHeart', tag: 'a', classe: 'baroque', active: false },
{ id: 'FabioSalso', tag: 'a', classe: 'classical', active: true },
{ id: 'CarlosSanchez', tag: 'span', classe: 'baroque', active: true },
{ id: 'RussellBell', tag: 'a', classe: 'classical', active: false },
{ id: 'JackDoe', tag: 'span', classe: 'baroque', active: true },
{ id: 'EricCarver', tag: 'a', classe: 'classical', active: false },
{ id: 'LouisDeschamps', tag: 'span', classe: 'baroque', active: true },
{ id: 'HoracioCane', tag: 'a', classe: 'baroque', active: true },
{ id: 'HenryBright', tag: 'a', classe: 'baroque', active: true },
]

179
subjects/get-them-all-dom/get-them-all.html

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html>
<head>
<title>Get them all</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
padding: 100px;
justify-content: center;
align-items: center;
font-size: 15px;
margin-top: 150px;
}
button {
outline: none;
border: none;
}
#buttons {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
top: 0;
height: 150px;
background: var(--background);
box-shadow: 0 0 50px black;
}
#buttons * {
margin: 0 20px;
padding: 10px 20px;
background: var(--clear);
border-radius: 20px;
cursor: pointer;
user-select: none;
width: 200px;
text-align: center;
}
.disabled {
pointer-events: none;
opacity: 0.3;
}
a,
span {
min-width: 110px;
min-height: 110px;
width: 5vw;
height: 5vw;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
border: solid 1px var(--clear);
line-height: 22px;
padding: 10px;
color: var(--clear);
margin: 30px;
}
.found {
box-shadow: 8px 8px 15px rgba(0, 0, 0, 0.6),
-10px -10px 15px rgba(255, 255, 255, 0.074);
border: none;
background: var(--purple);
color: var(--background);
}
</style>
</head>
<body>
<script type="module">
import { people } from './get-them-all.data.js'
import {
getBonannoPisano,
getActive,
getArchitects,
getClassical,
} from './get-them-all.js'
const body = document.querySelector('body')
const shuffle = (array) => {
const test = array.length - 1
for (let i = test; i > 0; i--) {
const j = Math.floor(Math.random() * i)
const temp = array[i]
array[i] = array[j]
array[j] = temp
}
return array
}
shuffle(people).map(({ id, classe, address, plans, tag, active }) => {
const people = document.createElement(tag)
people.id = id
people.textContent = 'Someone'
people.className = `${classe} ${active ? 'active' : ''}`
body.append(people)
})
const buttonsContainer = document.createElement('div')
buttonsContainer.id = 'buttons'
body.append(buttonsContainer)
const buttons = [
{ name: 'Architect', action: getArchitects },
{ name: 'Classical', action: getClassical },
{ name: 'Active', action: getActive },
{ name: 'Bonanno', action: getBonannoPisano },
]
buttons.forEach(({ name, action }, i) => {
const btn = document.createElement('button')
btn.id = `btn${name}`
btn.textContent = `Get ${name}${i === 0 ? 's' : ''}`
if (i > 0) {
btn.className = 'disabled'
}
btn.addEventListener('click', () => {
const [targetted, others] = action()
if (name === 'Bonanno') {
targetted.textContent = targetted.id.replace('P', ' P')
targetted.classList.add('found')
} else {
targetted.forEach((t) => {
t.textContent = name
})
}
others.forEach((o) => {
o.style.opacity = 0.2
})
btn.className = 'disabled'
const last = i + 1 === buttons.length
if (last) return
const next = document.getElementById(`btn${buttons[i + 1].name}`)
next.classList.remove('disabled')
})
buttonsContainer.append(btn)
})
</script>
</body>
</html>

38
subjects/gossip-grid-dom/README.md

@ -0,0 +1,38 @@
## Gossip grid
### Instructions
Good information is the pillar of society, that's why you've decided to dedicate your time to reveal the powerful truth to the world and deliver essential and strong news: you're launching a gossip grid.
Create the function `grid` which displays all the `gossips`, provided in the data file below, as cards on a grid (in the same order).
They must be `div` with the `gossip` class.
The first `gossip` card must be a `form` with a `textarea` and a submit button with the text `Share gossip!` that allows to add a new gossip to the list.
Create 3 `type="range"` inputs with the class `range`, all wrapped in a `div` with the class `ranges`:
- one with `id="width"` that control the width of cards _(from 200 to 800 pixels)_
- one with `id="fontSize"` that control the font size _(from 20 to 40 pixels)_
- one with `id="background"` that control the background lightness _(from 20% to 75%)_
> _tips:_ use `hsl` for colors
### Notions
- [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Form)
- [`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input): [`text`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text), [`range`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range)
### Files
You only need to create & submit the JS file `gossip-grid.js` ; we're providing you the following files to download (click right and save link) & test locally:
- the HTML file [gossip-grid.html](./gossip-grid.html) to open in the browser, which includes:
- the JS script which will allow to run your code
- some CSS pre-styled classes: feel free to use those as they are, or modify them
- the data file [gossip-grid.data.js](./gossip-grid.data.js) from which you can import `gossips`
### Expected result
You can see an example of the expected result [here](https://youtu.be/nbR2eHBqTxU)

19
subjects/gossip-grid-dom/gossip-grid.data.js

@ -0,0 +1,19 @@
export const gossips = [
`Oasis star Noel Gallagher used to gorge on Greggs pastries and donuts every day`,
`Lea Michele's lookalike Monica Moskatow says Glee star called her ugly`,
`WE PAY FOR JUICY INFO!`,
`Trainer to Hollywood's biggest stars reveals how to get an A-list body`,
`Ed Sheeran comes out of music retirement to write brand new song`,
`Kylie Jenner & Travis Scott’s breakup timeline`,
`Quiet on the set: temper tantrums stars hope you forget`,
`The style & grace of Chloë Grace Moretz: her top 20 red carpet looks`,
`Paulina Porizkova feels betrayed after being cut out of husband Ric Ocasek's will`,
`From too hot to not: Paris Hilton and Chris Zylka's relationship history`,
`No bite in the big apple? Celine Dion looks scary skinny in New York`,
`Jennifer Aniston and Brad Pitt relationship timeline`,
`They shouldn’t have said that: 10 celebrity rants heard around the world`,
`The most intense celebrity fights on set`,
`The 18 most bitter real housewives feuds`,
`Tristan Thompson's remarkable transformation from skinny teen to hulking NBA ace`,
`Kim Kardashian 'considers leaving home' with Kanye West to 'save marriage'`,
]

135
subjects/gossip-grid-dom/gossip-grid.html

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html>
<head>
<title>Gossip grid</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 100vw;
height: 100vh;
padding: 10rem;
align-items: flex-start;
}
.gossip {
background: hsl(280, 50%, 50%);
margin: 20px;
width: 250px;
padding: 20px 25px;
line-height: 30px;
font-size: 20px;
word-break: break-word;
color: white;
display: flex;
justify-content: space-between;
flex-direction: column;
}
.gossip:first-letter {
text-transform: uppercase;
}
.ranges {
position: fixed;
top: 5rem;
display: flex;
}
.range {
display: flex;
justify-content: center;
align-items: center;
color: var(--clear);
font-family: monospace;
}
.range label,
.range span {
width: 100px;
}
.range label {
text-align: right;
}
input {
margin: 20px 25px;
}
textarea {
height: 100%;
width: 100%;
max-width: 100%;
outline: none;
border: none;
font-family: inherit;
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
color: inherit;
background-color: transparent;
padding: 0;
margin: 0;
resize: none;
}
textarea::placeholder {
color: rgba(255, 255, 255, 0.5);
}
button {
margin-top: 20px;
text-align: right;
border: solid 1px white;
align-self: flex-end;
padding: 5px 15px;
cursor: pointer;
}
.fade-in {
animation: fade-in 0.75s;
}
@keyframes fade-in {
from {
opacity: 0%;
}
to {
opacity: 100%;
}
}
</style>
</head>
<body>
<script type="module">
import { grid } from './gossip-grid.js'
grid()
</script>
</body>
</html>

28
subjects/harder-bigger-bolder-stronger-dom/README.md

@ -0,0 +1,28 @@
## Harder, bigger, bolder, stronger
### Instructions
Being stuck at home, bored, desperate and coming up with a lot of weird ideas, a friend asks you to develop a tool to measure his ocular skills: one of those [Monoyer charts](https://en.wikipedia.org/wiki/Monoyer_chart) that ophthalmologists use.
Generate a board where each new letter is harder, bigger, bolder and stronger!
Write the function `generateLetters` which creates 120 `div`, each containing a letter randomly picked through the **uppercase** alphabet, and whose style properties have to be increased:
- each letter `font-size` has to grow from `11` to `130` pixels
- `font-weight` has to be `300` for the first third of the letters, `400` for the second third, and `600` for the last third
### Files
You only need to create & submit the JS file `harder-bigger-bolder-stronger.js` ; we're providing you the following file to download (click right and save link) & test locally:
- the HTML file [harder-bigger-bolder-stronger.html](./harder-bigger-bolder-stronger.html) to open in the browser, which includes:
- the JS script running some code, and which will also allow to run yours
- some CSS pre-styled classes: feel free to use those as they are, or modify them
### Notions
- [`createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)
- [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append)
- [`style`](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style)
- [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)

57
subjects/harder-bigger-bolder-stronger-dom/harder-bigger-bolder-stronger.html

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>Harder, bigger, bolder, stronger</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
width: 100vw;
padding: 5rem;
font-size: 12px;
}
div {
display: flex;
justify-content: center;
align-items: center;
margin: 5px;
width: 200px;
height: 200px;
color: white;
}
</style>
</head>
<body>
<script type="module">
import { generateLetters } from './harder-bigger-bolder-stronger.js'
generateLetters()
</script>
</body>
</html>

29
subjects/keycodes-symphony-dom/README.md

@ -0,0 +1,29 @@
## Keycodes symphony
### Instructions
Like an inspired Beethoven who's going to write his Moonlight Sonata, you're about to compose a colourful symphony of letters with your keyboard.
Write the function `compose`:
- Make it fire every time a key is pressed
- Create a new `div` with the class `note` when a letter of the lowercase alphabet is pressed, which has a unique background color generated using the `key` of the `event`, and displays the corresponding letter pressed
- If the pressed key is the `Backspace` one, delete the last note
- If the pressed key is the `Escape` one, clear all the notes
### Notions
- [Keyboard event](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent): [`keydown`](https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event), [`key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
### Files
You only need to create & submit the JS file `keycodes-symphony.js` ; we're providing you the following file to download (click right and save link) & test locally:
- the HTML file [keycodes-symphony.html](./keycodes-symphony.html) to open in the browser, which includes:
- the JS script which will allow to run your code
- some CSS pre-styled classes: feel free to use those as they are, or modify them
### Expected result
You can see an example of the expected result [here](https://youtu.be/5DdijwBnpAk)

57
subjects/keycodes-symphony-dom/keycodes-symphony.html

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>Keycodes symphony</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
width: 100vw;
height: 100vh;
}
.note {
width: 100%;
flex: 1 1 80px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 15px;
font-size: 40px;
color: white;
transition: all 0.2s ease-in-out;
}
</style>
</head>
<body>
<script type="module">
import { compose } from './keycodes-symphony.js'
compose()
</script>
</body>
</html>

35
subjects/mouse-trap-dom/README.md

@ -0,0 +1,35 @@
## Mouse trap
### Instructions
Develop a trap to capture the elements when the mouse is getting too close to the center of the page!
- Create a function `createCircle`: make it fire on every click on the page, and create a `div` at the position of the mouse on the screen, setting its `background` to `white` and its class to `circle`
- Create a function `moveCircle`: make it fire when the mouse moves, and get the last circle created and makes it move along with the mouse
- Create a function `setBox` which sets a box with the class `box` in the center of the page ; when a circle is inside that box, it has to be purple (use the CSS global variable `var(--purple)` as `background`) ; once a circle enters the box, it is trapped inside and cannot go out of it anymore.
> Hint: Be careful, a circle cannot overlap the box which has walls of `1px`, it has to be trapped **strictly** inside.
### Notions
- [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener): `click`, `mousemove`
- [`removeEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener)
- [Mouse event](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent): [`click`](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event), [`mousemove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event) / [`clientX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX), [`clientY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY)
- [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
### Provided files
### Files
You only need to create & submit the JS file `mouse-trap.js` ; we're providing you the following file to download (click right and save link) & test locally:
- the HTML file [mouse-trap.html](./mouse-trap.html) to open in the browser, which includes:
- the JS script which will allow to run your code
- some CSS pre-styled classes: feel free to use those as they are, or modify them
### Expected result
You can see an example of the expected result [here](https://youtu.be/qF843P-V2Yw)

63
subjects/mouse-trap-dom/mouse-trap.html

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<title>Mouse trap</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
width: 100vw;
height: 100vh;
padding: 5rem;
font-size: 12px;
}
.circle {
width: 50px;
height: 50px;
border-radius: 50%;
position: absolute;
opacity: 0.75;
}
.box {
width: 25vw;
height: 25vh;
border: solid 1px var(--clear);
}
</style>
</head>
<body>
<script type="module">
import { setBox, createCircle, moveCircle } from './mouse-trap.js'
setBox()
createCircle()
moveCircle()
</script>
</body>
</html>

79
subjects/nesting-organs-dom/README.md

@ -0,0 +1,79 @@
## Nesting organs
### Instructions
Bravo! You displayed the global shape of your entity, but now it's time to populate each division ; let's add up some organs!
To do so, we're going to introduce you the concept of nesting elements inside others.
So far, you just have a unique layer in your `<body>`: `face`, `upper-body` & `lower-body` are all at the same level.
But as you know, on a face, there are 2 eyes, a nose, and a mouth - and inside that mouth, a tongue, etc. ; any element can potentially be a container for other elements.
Let's add new elements and wrap them in different layers ; convert this list of organs in a HTML structure with the corresponding given tags!
- face: `section` tag with `id` `face`
- eyes: `div` tag with `id` `eyes`
- eye left: `p` tag with `id` `eye-left`
- eye right: `p` tag with `id` `eye-right`
- upper body: `section` tag with `id` `upper-body`
- arm left: `div` tag with `id` `arm-left`
- torso: `div` tag with `id` `torso`
- arm right: `div` tag with `id` `arm-right`
- lower body: `section` tag with `id` `lower-body`
- left leg: `div` tag with `id` `leg-left`
- right leg: `div` tag with `id` `leg-right`
Modify your CSS file to add rulesets to `section` tags: `display` at "flex" and `justify-content` at "center" (this is to turn the `section` tags into [`flex` containers](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox), so the elements inside will be centered)
Add also the following CSS to your CSS file to see the freshly-added nested elements:
```
div,
p {
border: solid 1px black;
padding: 10px;
margin: 0;
border-radius: 30px;
}
#face {
align-items: center;
}
#eyes {
display: flex;
background-color: yellow;
justify-content: space-between;
align-items: center;
border-radius: 50px;
width: 200px;
}
#torso {
width: 200px;
background-color: violet;
}
```
### Code examples
Nest several elements:
```html
<div id="first-element">
<span id="second-element"></span>
<div id="third-element">
<p id="fourth-element"></p>
</div>
</div>
```
### Expected output
This is what you should see in the browser:
![](https://github.com/01-edu/public/raw/master/subjects/nesting-organs/nesting-organs.png)
### Notions
- [Anatomy of an HTML element](https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Getting_started#anatomy_of_an_html_element)
- [Nesting HTML elements](https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Getting_started#nesting_elements)
- [Flexbox layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox), you can train on [Flexbox froggy](https://flexboxfroggy.com/)

BIN
subjects/nesting-organs-dom/nesting-organs.png

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 44 KiB

44
subjects/pick-and-click-dom/README.md

@ -0,0 +1,44 @@
## Pick & click
### Instructions
Today, you're gonna create your own color picker.
Write the function `pick` which turns the screen into a `hsl` color picker, varying the `hue` and `luminosity` of according to the position of the mouse, which:
- changes the `background` color of the `body`, so the `hsl` value is different on each mouse position on the screen:
- on the axis X, the hue value has to vary between 0 and 360
- on the axis Y, the luminosity value has to vary between 0 and 100
- displays those 3 values using the `text` class:
- the full `hsl` value in a `div` with the class `hsl` in the middle of the screen
- the `hue` value in a `div` with the class `hue` in the top right corner of the screen
- the `luminosity` value in a `div` with the class `luminosity` in the bottom left corner of the screen
- copies that value in the clipboard on click
- displays two SVG lines, with respective ids `axisX` and `axisY`, following the cursor like so:
- the axisX has to set the attributes `x1` and `x2` to the mouse X position
- the axisY has to set the attributes `y1` and `y2` to the mouse Y position
> Here is how a hsl value is formatted: `hsl(45, 50%, 35%)`
> Use `Math.round()` to round the values
### Notions
- [Copy event](https://developer.mozilla.org/en-US/docs/Web/API/Element/copy_event)
- [Mouse move event](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event)
- [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg): [`createElementNS`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS), [`setAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)
- Take a look at the [HSL section](https://developer.mozilla.org/en-US/docs/Web/HTML/Applying_color)
### Files
You only need to create & submit the JS file `pick-and-click.js` ; we're providing you the following file to download (click right and save link) & test locally:
- the HTML file [pick-and-click.html](./pick-and-click.html) to open in the browser, which includes:
- the JS script which will allow to run your code
- some CSS pre-styled classes: feel free to use those as they are, or modify them
### Expected result
You can see an example of the expected result:
[![video](https://img.youtube.com/vi/eE4eE9_eKZI/0.jpg)](https://www.youtube.com/watch?v=eE4eE9_eKZI)

86
subjects/pick-and-click-dom/pick-and-click.html

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head>
<title>Pick & click</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
width: 100vw;
height: 100vh;
padding: 5rem;
cursor: crosshair;
}
svg {
filter: invert(100%);
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
}
svg line {
stroke-width: 0.6px;
stroke: grey;
}
.text {
position: fixed;
filter: invert(100%);
font-size: 50px;
cursor: pointer;
white-space: pre-wrap;
}
.hsl {
filter: invert(100%);
font-size: 17px;
}
.hue {
top: 100px;
right: 100px;
text-align: right;
}
.luminosity {
bottom: 100px;
left: 100px;
}
</style>
</head>
<body>
<script type="module">
import { pick } from './pick-and-click.js'
pick()
</script>
</body>
</html>

55
subjects/pimp-my-style-dom/README.md

@ -0,0 +1,55 @@
## Pimp my style
### Instructions
Check out that button on the HTML page:
```html
<button class="button">pimp my style</div>
```
For now, it's only a lonely, basic and sad element ; let's pimp it up!
On each click on the page, a function `pimp` is triggered.
Write the body of that function so that the button's class is altered:
- Add in order the next class of the `styles` array provided in the data file below
- When the end of the array is reached, remove backwards each class
- Toggle the class 'unpimp' when removing classes
```
Example for a `styles` array with only 3 classes:
Page load --> <button class="button"></div>
...adding
Click 1 --> <button class="button one"></div>
Click 2 --> <button class="button one two"></div>
...toggling `unpimp`
Click 3 --> <button class="button one two three unpimp"></div>
...and removing backwards
Click 4 --> <button class="button one two unpimp"></div>
Click 5 --> <button class="button one unpimp"></div>
Click 6 --> <button class="button"></div>
```
### Notions
- [`classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList): `add()`, `remove()`, `toggle()`
### Files
You only need to create & submit the JS file `pimp-my-style.js` ; we're providing you the following files to download (click right and save link) & test locally:
- the HTML file [pimp-my-style.html](./pimp-my-style.html) to open in the browser, which includes:
- the JS script running some code, and which will also allow to run yours
- some CSS pre-styled classes: feel free to use those as they are, or modify them
- the data file [pimp-my-style.data.js](./pimp-my-style.data.js) from which you can import `styles`
### Expected result
You can see an example of the expected result [here](https://youtu.be/VIRf3TBDTN4)

17
subjects/pimp-my-style-dom/pimp-my-style.data.js

@ -0,0 +1,17 @@
export const styles = [
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
]

181
subjects/pimp-my-style-dom/pimp-my-style.html

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html>
<head>
<title>Pimp my style</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
width: 100vw;
height: 100vh;
color: var(--text);
padding: 2.5rem 0;
}
button {
user-select: none;
letter-spacing: 0;
color: white;
outline: none;
border: none;
}
.button:first-letter {
text-transform: uppercase;
}
.button.unpimp:before {
content: 'Un';
}
.button {
background: var(--background);
font-family: serif;
width: 70%;
text-align: center;
}
.one {
font-size: 75px;
}
.two {
font-family: sans-serif;
}
.three {
letter-spacing: 15px;
}
.four {
padding: 20px 40px;
border: solid 1px var(--clear);
}
.five {
border-radius: 100px;
}
.six {
border: none;
box-shadow: 8px 8px 15px rgba(255, 255, 255, 0.075),
-10px -10px 15px rgba(0, 0, 0, 0.3);
}
.seven {
color: var(--purple);
}
.eight {
border: solid 0.5px var(--purple);
}
.nine {
background: var(--purple);
color: white;
position: relative;
box-shadow: 0px 0px 35px rgba(0, 0, 0, 0.8);
}
.ten:after {
position: absolute;
content: '';
width: 100%;
height: 100%;
padding: 15px;
top: -16px;
left: -16px;
border-radius: 100px;
border: solid 1px var(--clear);
}
.eleven:after {
position: absolute;
content: '';
padding: 30px;
top: -31px;
left: -31px;
background: white;
z-index: -1;
}
.twelve {
color: var(--background);
}
.thirteen {
text-decoration: underline;
}
.fourteen {
animation: animation 1.5s linear infinite;
background: linear-gradient(
to right,
var(--purple) 0%,
white 48%,
white 52%,
var(--purple) 100%
);
background-size: 500px 640px;
position: relative;
}
@keyframes animation {
0% {
background-position: 0 0;
}
100% {
background-position: 500px 0;
}
}
.fifteen {
height: 100vh;
width: 100vw;
border-radius: 0;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<script type="module">
import { pimp } from './pimp-my-style.js'
const body = document.querySelector('body')
const button = document.createElement('button')
button.className = 'button'
button.textContent = 'pimp my style'
body.append(button)
button.addEventListener('click', pimp)
</script>
</body>
</html>

81
subjects/select-and-style-dom/README.md

@ -0,0 +1,81 @@
## Select & style
### Resources
We provide you with some content to get started smoothly, check it out!
- Video [Link a CSS stylesheet to your HTML file](https://www.youtube.com/watch?v=e7G-KhaqTjs&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=3)
- Video [CSS - Style with type selectors](https://www.youtube.com/watch?v=q0ur7YWBzhs&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=4)
- Video [HTML/CSS - Set & style with ID selector](https://www.youtube.com/watch?v=3b3MiY-MR-Y&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=5)
### Instructions
Now that you created & identified properly the different sections of your being,
it's time to make it look more living-like! To achieve that, you're going to
style it with [CSS][0]. Create a CSS file, [link it][1] to your
`select-and-style.html`, and:
- target all the elements with the [universal selector][2] and style them with:
- `margin` of `0`
- `box-sizing` to `border-box`
- `opacity` of `0.85`
- target the `body` tag and style it with a `height` of `100vh` so it takes the
viewport height
- target all the `section` tags with the [type selector][3], and style it with:
- `padding` of `20px`
- `width` of `100%`
- `height` of `calc(100% / 3)` _(one third of the `body` height)_
- target each following element with the [`id` selector][4], using the `id` you
defined earlier for each section, and style it:
- `face` with a "cyan" `background-color`
- `upper-body` with a "blueviolet" `background-color`
- `lower-body` with a "lightsalmon" `background-color`
### Code examples
To style an element, you systematically have to declare [rulesets][5], composed of a property and a value.
Set the color of `div` tags to `"red"`:
```css
div {
color: red;
}
```
Set the `background-color` of the HTML element with the `id` `"block-1"`:
```css
#block-1 {
color: red;
}
```
### Expected output
This is what you should see in the browser: ![screenshot][8]
### Notions
- [`link` a CSS file][1]
- [CSS basics][7]
- [ruleset][5]
- [List of different selectors][6]
- [universal selector][2]
- [type selector][3]
- [`id` selector][4]
[0]: https://developer.mozilla.org/en-US/docs/Web/CSS
[1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#including_a_stylesheet
[2]: https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors
[3]: https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors
[4]: https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors
[5]: https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics#anatomy_of_a_css_ruleset
[6]: https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics#different_types_of_selectors
[7]: https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics
[8]: https://github.com/01-edu/public/blob/master/subjects/select-and-style/select-and-style.png?raw=true

BIN
subjects/select-and-style-dom/select-and-style.png

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 37 KiB

81
subjects/skeleton-dom/README.md

@ -0,0 +1,81 @@
## Skeleton
### Welcome
Welcome to the `world-wide-what` quest!
In this new digital world you're gonna discover, it is possible to create beings with some lines of code. Yes, it is.
Like a modern Dr Frankenstein, you're about to dive into the `world-wide-what` and give birth to a new entity on the virtual space of your browser.
During this quest, step by step, you are going to shape it.
But before coding anything, make sure you checked the videos of the playlist; you need to have a server running on your computer.
For the whole quest, the principle is to iterate over your code: when you finish an exercise, copy-paste your code to use it in the next one.
### Resources
We provide you with some content to get started smoothly, check it out!
- Video [Basic set up of an HTML page](https://www.youtube.com/watch?v=QtKoO7tT-Gg&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=1)
- Video [Different HTML tags overview](https://www.youtube.com/watch?v=Al-Jzpib8VY&list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF&index=2)
Those videos are accompanying you step by step in each exercise, but if you want to check right away all the notions covered in the quest, you can watch the whole playlist [Web - HTML, CSS & DOM JS](https://www.youtube.com/playlist?list=PLHyAJ_GrRtf979iZZ1N3qYMfsPj9PCCrF).
### Instructions
Ready? Let's code!
The first step to achieve in your quest is to conceive your being ; for that, 2 things have to be done:
- set some parameters (`<head>` tag)
- define the structure, or skeleton (`<body>` tag)
#### Parameters
To create any project, some things need to be declared in the HTML file - those are not visible elements in your page, but things cannot work without them.
Set your page with [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) tags, and also put a [`<title>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title) to name the entity you're going to create.
#### Structure
Now you have to define the skeleton of your future creation.
Remember this as a general rule for any further project you will start: a good way of setting up a project is to think about all the elements needed, organize and divide them in blocks.
Let's define the first level of elements that your entity will be made of ; we're going to split it into 3 main chunks: the face, the upper body, and the lower body.
Inside the `<body>` tag of your HTML file, create 3 divisions using `<section>` tags, putting the following text content inside for each: `face`, `upper-body`, `lower-body`.
If you open you HTML file in the browser, you should see those 3 texts appear on the screen.
### Code examples
Create a `div` tag with `hello` as text content inside the `body`:
```html
<html>
<body>
<div>hello</div>
</body>
</html>
```
#### How to submit
This exercise must be submited as a JS file, thankfully, javascript allows you to write `HTML` !
Here is how you would submit the `HTML` sample from above in the editor:
```js
document.documentElement.innerHTML = `
<body>
<div>hello</div>
</body>
`
```
> You do only need to write what's inside the `<html>` tag excluding the `<html>` tag itself
### Notions
- [`html` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html)
- [`head` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)
- [`title` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title)
- [`body` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body)
- [`section` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section)

40
subjects/the-calling-dom/README.md

@ -0,0 +1,40 @@
## The calling
### Instructions
Congrats! You created the very first base for your being and you witnessed its
appearance on the digital world - your browser. But so far, it's a tiny seed of
the marvelous thing it could become ; be patient, there still is a bit of work.
First of all, instead of writing down what things are _(you're not writing down
on your hand the word 'hand', are you?)_ we're going to identify them
semantically with the very practical
[`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)
attribute. \
This `id` has to be a **unique** identifier in your webpage, which will allow you
to target your element if needed. Your can compare it to your name & surname ; this
is what is identifying you to other people, and if someone's calling you by your
name, you answer.
So let's identify the 3 elements we have so far: in each section, remove the
text content from inside the tag and set it as the value of the `id` attribute
of the corresponding `section` text.
**Open the page in the browser:** \
you don't see _anything_? Don't freak out! \
Inspect the HTML that have been created with your
[browser inspector tool](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools),
& if you done it correctly, you should see inside the `body` the 3 sections with
the `id` attribute set in your HTML structure.
### Code examples
Set the `id` of a `div` tag to `"my-lil-div"`:
```html
<div id="my-lil-div"></div>
```
### Notions
- [`id` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)

43
subjects/where-do-we-go-dom/README.md

@ -0,0 +1,43 @@
## Where do we go?
### Instructions
Tired of staying home for too long, you decide to develop a page to index ideas for your next travel destinations, so that next time you'll ask yourself 'Where do we go?', you won't need to get lost for 3 hours!
Write the function `explore` which creates a page displaying the list of `places` provided in the data file below:
- sort the `places` from the Northest to the Southest
- display a fullscreen-size `<section>` for each place ; use the pics hosted in the `./where-do-we-go_images` folder (find the download link below) to set the `background` attribute with the corresponding image URL. The URL has to be formatted like so: `./where-do-we-go_images/name-of-the-place.jpg`
- display a location indicator as a `<a>` tag in the middle of the screen which:
- has the class `location`
- displays as text strings separated by `\n`, the `name` and the `coordinates` of the current place featured in the image
- using the corresponding `color` as text color
- updates the `name`, `coordinates` and `color` on scroll, when the top of the next image reaches the middle of the screen height
- has the `href` attribute set to open **a new tab** redirecting to a Google Maps' URL with the coordinates of the place currently displayed
- display a compass as a `div` tag indicating the latitude direction which:
- has the class `direction`
- displays `N` for North if the user is scrolling up
- displays `S` for South if he's scrolling down
### Notions
- [Scroll event](https://developer.mozilla.org/en-US/docs/Web/API/Element/scroll_event)
- [`window`](https://developer.mozilla.org/en-US/docs/Web/API/Window): [`innerHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Window/innerHeight), [`scrollY`](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY), [`pageYOffset`](https://developer.mozilla.org/en-US/docs/Web/API/Window/pageYOffset)
- Take a look at the [DMS coordinates system](https://en.wikipedia.org/wiki/Decimal_degrees)
### Files
You only need to create & submit the JS file `where-do-we-go.js` ; we're providing you the following files to download (click right and save link) & test locally:
- the HTML file [where-do-we-go.html](./where-do-we-go.html) to open in the browser, which includes:
- the JS script which will allow to run your code
- some CSS pre-styled classes: feel free to use those as they are, or modify them
- the data file [where-do-we-go.data.js](./where-do-we-go.data.js) from which you can import `places`
- the images to use, in this [compressed folder](https://assets.01-edu.org/where-do-we-go_images.zip)
### Expected result
You can see an example of the expected result [here](https://youtu.be/BLxNi1WH6_0)

107
subjects/where-do-we-go-dom/where-do-we-go.data.js

@ -0,0 +1,107 @@
export const places = [
{
name: 'Cordoba, Spain',
color: 'deeppink',
coordinates: `37°53'17.43"N 4°46'45.78"W`,
},
{
name: 'Yuanyang County, China',
color: 'cyan',
coordinates: `23°09'32.30"N 102°44'41.46"E`,
},
{
name: 'Namib Desert, Namibia',
color: 'lime',
coordinates: `24°45'4.19"S 15°16'21.00"E`,
},
{
name: 'Newark, New Jersey, USA',
color: 'yellow',
coordinates: `40°44'8.37"N 74°10'20.52"W`,
},
{
name: 'Nishinoshima Island, Japan',
color: 'lightcoral',
coordinates: `27°14'50.84"N 140°52'46.04"E`,
},
{
name: 'Lisse, The Netherlands',
color: 'cornflowerblue',
coordinates: `52°15'28.55"N 4°33'26.94"E`,
},
{
name: 'Shadegan Lagoon, Iran',
color: 'firebrick',
coordinates: `30°39'16.55"N 48°39'14.14"E`,
},
{
name: 'Qinhuangdao, China',
color: 'seashell',
coordinates: `39°56'7.3"N 119°36'1.88"E`,
},
{
name: 'Marrakesh, Morocco',
color: 'orange',
coordinates: `31°37'46.1"N 7°58'51.9"W`,
},
{
name: 'Los Caracoles Pass, Chile',
color: 'violet',
coordinates: `32°49'51.6"S 70°05'22.9"W`,
},
{
name: 'Tucson, Arizona, USA',
color: 'springgreen',
coordinates: `32°13'21.38"N 110°58'28.96"W`,
},
{
name: 'Arlit, Niger',
color: 'blue',
coordinates: `18°44'20.41"N 7°23'22.12"E`,
},
{
name: 'Black Rock Desert, Nevada, USA',
color: 'crimson',
coordinates: `40°54'35.0"N 119°03'26.5"W`,
},
{
name: 'Mount Fuji, Japan',
color: 'darkviolet',
coordinates: `35°21'37.0"N 138°43'38.1"E`,
},
{
name: 'Moab, Utah, USA',
color: 'gold',
coordinates: `38°34'23.94"N 109°32'59.42"W`,
},
{
name: 'Rio de Janeiro, Brasil',
color: 'hotpink',
coordinates: `22°59'13.4"S 43°12'15.9"W`,
},
{
name: 'Killeen, Texas, USA',
color: 'greenyellow',
coordinates: `31°07'1.63"N 97°43'40.07"W`,
},
{
name: 'Skafta River, Iceland',
color: 'mistyrose',
coordinates: `63°39'47.7"N 17°47'57.9"W`,
},
{
name: 'Almeria, Spain',
color: 'mediumturquoise',
coordinates: `36°50'08.7"N 2°27'44.8"W`,
},
{
name: 'Atlanta, Georgia, USA',
color: 'white',
coordinates: `33°45'39.0"N 84°23'50.1"W`,
},
{
name: 'Georgetown, California, USA',
color: 'sandybrown',
coordinates: `38°54'22.4"N 120°50'23.9"W`,
},
]

72
subjects/where-do-we-go-dom/where-do-we-go.html

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<title>Where do we go?</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link id="fav" rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;,">
<style>
:root {
--background: hsl(0, 0%, 12%);
--text: hsl(0, 0%, 80%);
--clear: hsl(0, 0%, 65%);
--disabled: hsl(0, 0%, 35%);
--purple: #bb73e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
letter-spacing: 1.5px;
background: var(--background);
}
section {
height: 100vh;
width: 100vw;
}
a {
text-decoration: none;
}
.location {
position: fixed;
top: 0;
color: white;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 100px;
line-height: 130px;
white-space: pre-wrap;
text-align: center;
cursor: pointer;
}
.direction {
position: fixed;
right: 100px;
top: 100px;
text-align: center;
font-size: 40px;
line-height: 60px;
color: white;
white-space: pre-wrap;
}
</style>
</head>
<body>
<script type="module">
import { explore } from './where-do-we-go.js'
explore()
</script>
</body>
</html>

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/almeria.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 206 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/arlit.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 155 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/atlanta.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 218 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/black-rock-desert.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 244 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/cordoba.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 610 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/georgetown.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 248 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/killeen.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 296 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/lisse.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 166 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/los-caracoles-pass.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 209 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/marrakesh.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 444 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/moab.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 223 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/mount-fuji.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 133 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/namib-desert.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 164 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/newark.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 227 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/nishinoshima-island.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 222 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/qinhuangdao.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 126 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/rio-de-janeiro.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 282 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/shadegan-lagoon.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 78 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/skafta-river.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 163 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/tucson.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 168 KiB

BIN
subjects/where-do-we-go-dom/where-do-we-go_images/yuanyang-county.jpg

diff.bin_not_shown

After

Width:  |  Height:  |  Size: 197 KiB

Loading…
Cancel
Save