├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── assets │ ├── css-speedrun.png │ ├── favicon.png │ ├── favicon.svg │ └── prism.css ├── js │ ├── firstHint.js │ ├── main.js │ └── puzzles │ │ ├── index.js │ │ ├── level0.js │ │ ├── level1.js │ │ ├── level10.js │ │ ├── level2.js │ │ ├── level3.js │ │ ├── level4.js │ │ ├── level5.js │ │ ├── level6.js │ │ ├── level7.js │ │ ├── level8.js │ │ └── level9.js ├── scss │ ├── _footer.scss │ ├── _header.scss │ ├── _main.scss │ ├── _reset.scss │ └── index.scss └── views │ ├── index.html │ └── privacy.html └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vincent Will 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Speedrun 2 | 3 | A small fun app to test your CSS knowledge. Find the correct CSS selectors for the 10 puzzles as fast as possible. 4 | 5 | [https://css-speedrun.netlify.app](https://css-speedrun.netlify.app) 6 | 7 | ## Setup 8 | 9 | - Install the app with `npm i` 10 | - run `npm run build` to create the dist directory 11 | - run `npm run watch` to run the dev server and watch for changes 12 | 13 | 14 | ## Create your own puzzles 15 | 16 | To create your own puzzles check the files in `/src/js/puzzles`. 17 | 18 | They contain the code for the puzzle and an array to mark which lines should be selected. 19 | Also you can provide an optional hint to help others solve your puzzle. 20 | 21 | ## Solutions 22 | 23 |
24 | Answer list: 25 | Intro: li:first-child 26 | 38 |
39 | 40 | ## License 41 | 42 | [MIT](https://choosealicense.com/licenses/mit/) 43 | 44 | --- 45 | 46 | *created by [Vincent Will](https://wweb.dev/)* 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-speedrun", 3 | "version": "1.0.0", 4 | "description": "A small project to test your CSS skiulls", 5 | "main": "index.js", 6 | "scripts": { 7 | "css:autoprefixer": "postcss -u autoprefixer -r dist/*.css", 8 | "css:scss": "node-sass --output-style compressed -o dist src/scss", 9 | "build:assets": "copyfiles src/assets/* dist/assets --flat", 10 | "build:css": "npm run css:scss && npm run css:autoprefixer", 11 | "build:html": "copyfiles src/views/*.html dist --flat", 12 | "build:js": "webpack --mode=production", 13 | "build": "run-s build:*", 14 | "serve": "browser-sync start --server \"dist\" --files \"dist\"", 15 | "watch:assets": "onchange \"src/assets\" -- npm run build:assets", 16 | "watch:css": "onchange \"src/scss\" -- npm run build:css", 17 | "watch:html": "onchange \"src/views\" -- npm run build:html", 18 | "watch:js": "onchange \"src/js\" -- webpack --mode=development", 19 | "watch": "run-p serve watch:*" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/Vincenius/css-speedrun" 24 | }, 25 | "author": "", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/vincenius/css-speedrun/issues" 29 | }, 30 | "homepage": "https://css-speedrun.netlify.app/", 31 | "devDependencies": { 32 | "@babel/preset-env": "^7.15.8", 33 | "autoprefixer": "^10.3.7", 34 | "babel-loader": "^8.2.2", 35 | "browser-sync": "^2.27.5", 36 | "copyfiles": "^2.4.1", 37 | "imagemin-cli": "^7.0.0", 38 | "node-sass": "^7.0.1", 39 | "npm-run-all": "^4.1.5", 40 | "onchange": "^7.1.0", 41 | "postcss": "^8.3.9", 42 | "postcss-cli": "^9.0.1", 43 | "webpack": "^5.58.1", 44 | "webpack-cli": "^4.9.0" 45 | }, 46 | "dependencies": { 47 | "@popperjs/core": "^2.11.2", 48 | "easytimer.js": "^4.5.1", 49 | "js-confetti": "^0.10.2", 50 | "prismjs": "^1.26.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vincenius/css-speedrun/223ffbc9cef88a7dac1590700df6dd372b70ab2f/src/.DS_Store -------------------------------------------------------------------------------- /src/assets/css-speedrun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vincenius/css-speedrun/223ffbc9cef88a7dac1590700df6dd372b70ab2f/src/assets/css-speedrun.png -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vincenius/css-speedrun/223ffbc9cef88a7dac1590700df6dd372b70ab2f/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.26.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css */ 3 | code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} 4 | -------------------------------------------------------------------------------- /src/js/firstHint.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import Prism from 'prismjs' 2 | import Timer from 'easytimer.js' // https://albert-gonzalez.github.io/easytimer.js/ 3 | import JSConfetti from 'js-confetti' 4 | import { createPopper } from '@popperjs/core'; 5 | import puzzles from './puzzles' 6 | 7 | const jsConfetti = new JSConfetti() 8 | const timer = new Timer({ precision: 'secondTenths' }) 9 | 10 | let levelIndex = 0 11 | let finalResult = '' 12 | let isLevelSuccess = false 13 | let hintTimeout1 14 | let hintTimeout2 15 | const results = [] 16 | const htmlGoal = document.querySelector('#html-goal') 17 | const htmlInput = document.querySelector('#html-preview') 18 | const verification = document.querySelector('#verification') 19 | const submitButton = document.querySelector('#submit') 20 | const cssInput = document.querySelector('#css-input') 21 | const timebox = document.querySelector('#timer') 22 | const levelContainer = document.querySelector('#levels') 23 | const hintLink1 = document.querySelector('#hint1') 24 | const hintLink2 = document.querySelector('#hint2') 25 | const solution = document.querySelector('#solution') 26 | const solutionCode = document.querySelector('#solution-code') 27 | const nextLevel = document.querySelector('#next-level') 28 | const tooltip = document.querySelector('#tooltip') 29 | 30 | const levelItems = puzzles 31 | .map((p, i) => `
  • 32 | ${i === 0 ? 'Intro' : `Level ${i}`} 33 | 34 |
  • `) 35 | .join(' ') 36 | levelContainer.innerHTML = levelItems 37 | 38 | const getFormattedNumber = i => i.toString().padStart(2, 0) 39 | 40 | const levelSuccess = () => { 41 | solutionCode.innerHTML = Prism.highlight(puzzles[levelIndex].solution, Prism.languages.markup, 'css'); 42 | 43 | levelIndex++; 44 | isLevelSuccess = true; 45 | timer.pause(); 46 | results.push(Object.assign({}, timer.getTimeValues())) 47 | solution.classList.remove('hidden') 48 | cssInput.classList.add('success') 49 | clearTimeout(hintTimeout1) 50 | clearTimeout(hintTimeout2) 51 | 52 | if (levelIndex === 1) { 53 | nextLevel.classList.remove('hidden') 54 | } 55 | 56 | if (levelIndex === puzzles.length) { 57 | // last level done 58 | finalResult = timer.getTimeValues().toString(['minutes', 'seconds', 'secondTenths']) 59 | timer.stop() 60 | 61 | jsConfetti.addConfetti() 62 | jsConfetti.addConfetti({ 63 | emojis: ['🌈', '✨', '🦄'], 64 | emojiSize: 50, 65 | confettiNumber: 50, 66 | }) 67 | timebox.classList.add('done') 68 | submitButton.setAttribute('disabled', true) 69 | cssInput.setAttribute('disabled', true) 70 | 71 | generateWinScreen() 72 | } 73 | 74 | // update level sidebar 75 | for (let level of document.querySelectorAll('#levels > li')) { 76 | const levelNumber = parseInt(level.getAttribute('data-level')) 77 | 78 | if (levelNumber === levelIndex) { 79 | level.classList.add('active') 80 | } 81 | if (levelNumber === (levelIndex - 1)) { 82 | level.classList.remove('active') 83 | level.classList.add('done') 84 | const prevResult = results[levelNumber - 1] || { minutes: 0, seconds: 0, secondTenths: 0 } 85 | const newResult = results[levelNumber] 86 | const difference = (newResult.secondTenths - prevResult.secondTenths) 87 | + ((newResult.seconds - prevResult.seconds) * 10) 88 | + ((newResult.minutes - prevResult.minutes) * 600) 89 | const minutes = parseInt(difference / 600) 90 | const seconds = parseInt((difference - (minutes * 600)) / 10) 91 | const secondTenths = parseInt(difference - (minutes * 600) - (seconds * 10)) 92 | 93 | const resultTime = `${getFormattedNumber(minutes)}:${getFormattedNumber(seconds)}:${secondTenths}` 94 | 95 | timebox.setAttribute('data-before', resultTime); 96 | timebox.classList.add('success'); 97 | setTimeout(() => { timebox.classList.remove('success')}, 1500) 98 | 99 | level.querySelector('.timeResult').innerHTML = `[${resultTime}]` 100 | } 101 | } 102 | } 103 | 104 | const initLevel = () => { 105 | isLevelSuccess = false 106 | cssInput.classList.remove('success') 107 | solution.classList.add('hidden') 108 | nextLevel.classList.add('hidden') 109 | 110 | if (levelIndex >= 1) { 111 | timer.start() 112 | } 113 | 114 | // load next level 115 | cssInput.removeAttribute('disabled') 116 | htmlInput.innerHTML = Prism.highlight(puzzles[levelIndex].code, Prism.languages.markup, 'markup'); 117 | htmlGoal.innerHTML = puzzles[levelIndex].goal.reduce((acc, curr) => acc + (curr ? '➡️\n' : '\n'), ''); 118 | verification.innerHTML = puzzles[levelIndex].verificationCode; 119 | 120 | hintLink1.classList.remove('fade-in') 121 | hintLink2.classList.remove('fade-in') 122 | clearTimeout(hintTimeout1) 123 | clearTimeout(hintTimeout2) 124 | 125 | if (puzzles[levelIndex].hint1) { 126 | tooltip.innerHTML = puzzles[levelIndex].hint1 127 | hintTimeout1 = setTimeout(() => { 128 | hintLink1.classList.add('fade-in') 129 | }, 10000) // show hint after 10 secs 130 | } 131 | if (puzzles[levelIndex].hint2) { 132 | hintLink2.setAttribute('href', puzzles[levelIndex].hint2) 133 | hintTimeout2 = setTimeout(() => { 134 | hintLink2.classList.add('fade-in') 135 | }, 20000) // show hint after 20 secs 136 | } 137 | 138 | cssInput.value = ''; 139 | } 140 | 141 | const checkLevel = () => { 142 | const cssValue = cssInput.value 143 | let selectedHtml 144 | try { 145 | selectedHtml = verification.querySelectorAll(`div ${cssValue}`) 146 | } catch (e) { 147 | // ignore invalid css 148 | cssInput.classList.add('error') 149 | selectedHtml = [] 150 | } 151 | const selectedRows = Array.from(selectedHtml).map(elem => parseInt(elem.getAttribute('data-row'))) 152 | 153 | const result = puzzles[levelIndex].goal.map((expectedResult, i) => 154 | selectedRows.includes(i) === expectedResult 155 | ) 156 | const completedLevel = result.every(r => r) 157 | 158 | // use loop to keep it readable 159 | let resultString = '' 160 | let rowResult 161 | for (let i = 0; i < puzzles[levelIndex].goal.length; i++) { 162 | if (puzzles[levelIndex].goal[i]) { // should be selected 163 | rowResult = result[i] ? '
  • ' : '
  • ' 164 | } else { // should not be selected 165 | rowResult = result[i] ? '
  • ' : '
  • ' 166 | } 167 | resultString += rowResult 168 | } 169 | 170 | if (!htmlInput.querySelector('.check')) { 171 | htmlInput.innerHTML = htmlInput.innerHTML + '' 172 | } 173 | htmlInput.querySelector('.check').innerHTML = resultString 174 | 175 | if (completedLevel) { 176 | levelSuccess() 177 | } 178 | } 179 | 180 | const generateWinScreen = () => { 181 | const tweetLink = document.querySelector('#share-tweet') 182 | const winTweetText = `I've solved all #CSS puzzles on CSS Speedrun™ within ${finalResult} and all I got was this stupid tweet. 183 | 184 | https://css-speedrun.netlify.app/` 185 | 186 | tweetLink.setAttribute('href', `https://twitter.com/intent/tweet?text=${encodeURI(winTweetText).replace('#', '%23')}`) 187 | // document.querySelector('#code-screen').classList.add('hidden') 188 | document.querySelector('#result-screen').classList.remove('hidden') 189 | } 190 | 191 | initLevel() 192 | 193 | submitButton.addEventListener('click', () => { 194 | cssInput.classList.remove('error') 195 | if (isLevelSuccess) { 196 | initLevel() 197 | } else { 198 | checkLevel() 199 | } 200 | }) 201 | cssInput.addEventListener('keypress', e => { 202 | cssInput.classList.remove('error') 203 | if (e.keyCode === 13) { // check on enter 204 | if (isLevelSuccess) { 205 | initLevel() 206 | } else { 207 | checkLevel() 208 | } 209 | } 210 | }) 211 | 212 | timer.addEventListener('secondTenthsUpdated', function (e) { 213 | timebox.innerHTML = timer.getTimeValues().toString(['minutes', 'seconds', 'secondTenths']); 214 | }) 215 | 216 | // TOOLTIP HINT 217 | const popperInstance = createPopper(hintLink1, tooltip, { 218 | placement: 'bottom-end', 219 | modifiers: [ 220 | { 221 | name: 'offset', 222 | options: { 223 | offset: [0, 8], 224 | }, 225 | }, 226 | ], 227 | }); 228 | 229 | function show() { 230 | tooltip.setAttribute('data-show', ''); 231 | popperInstance.update(); 232 | } 233 | 234 | function hide() { 235 | tooltip.removeAttribute('data-show'); 236 | } 237 | 238 | const showEvents = ['mouseenter', 'focus']; 239 | const hideEvents = ['mouseleave', 'blur']; 240 | 241 | showEvents.forEach((event) => { 242 | hintLink1.addEventListener(event, show); 243 | }); 244 | 245 | hideEvents.forEach((event) => { 246 | hintLink1.addEventListener(event, hide); 247 | }); -------------------------------------------------------------------------------- /src/js/puzzles/index.js: -------------------------------------------------------------------------------- 1 | import level0 from './level0.js' 2 | import level1 from './level1.js' 3 | import level2 from './level2.js' 4 | import level3 from './level3.js' 5 | import level4 from './level4.js' 6 | import level5 from './level5.js' 7 | import level6 from './level6.js' 8 | import level7 from './level7.js' 9 | import level8 from './level8.js' 10 | import level9 from './level9.js' 11 | import level10 from './level10.js' 12 | 13 | 14 | export default [ 15 | level0, 16 | level1, 17 | level2, 18 | level3, 19 | level4, 20 | level5, 21 | level6, 22 | level7, 23 | level8, 24 | level9, 25 | level10, 26 | ].map(level => ({ 27 | ...level, 28 | verificationCode: level.code.split('\n').map((row, i) => row.replace('>', ` data-row="${i}">`)).join(' ') 29 | })) -------------------------------------------------------------------------------- /src/js/puzzles/level0.js: -------------------------------------------------------------------------------- 1 | const code = `` 6 | 7 | export default { 8 | code, 9 | goal: [false, true, false, false, false], 10 | hint1: 'Try a pseudo class that only selects
    the first child', 11 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child', 12 | solution: 'li:first-child', 13 | } 14 | -------------------------------------------------------------------------------- /src/js/puzzles/level1.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 |

    3 |

    4 |

    5 |

    6 |
    ` 7 | 8 | export default { 9 | code, 10 | goal: [false, true, false, true, true, false], 11 | hint1: 'Use a CSS pseudo class that will not
    match the class', 12 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/:not', 13 | solution: 'p:not(.foo)', 14 | } 15 | -------------------------------------------------------------------------------- /src/js/puzzles/level10.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 |
    3 | 4 | 5 |
    6 |
    7 | 8 | 9 | 10 |
    11 |
    12 | 13 | 14 |
    15 | 16 | 17 |
    ` 18 | 19 | export default { 20 | code, 21 | goal: [false, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false], 22 | hint1: 'Use a combination of the things you used before', 23 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors', 24 | solution: 'div div span + code:not(.foo)', 25 | } 26 | -------------------------------------------------------------------------------- /src/js/puzzles/level2.js: -------------------------------------------------------------------------------- 1 | const code = `` 10 | 11 | export default { 12 | code, 13 | goal: [false, false, false, true, false, true, false, true ,false], 14 | hint1: 'Use a CSS pseudo class that matches elements
    based on their position', 15 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child', 16 | solution: 'li:nth-child(2n + 3)', 17 | } 18 | -------------------------------------------------------------------------------- /src/js/puzzles/level3.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 | 3 |

    4 | 5 | 6 |

    7 |
    ` 8 | 9 | export default { 10 | code, 11 | goal: [false, true, true, false, false, false, false], 12 | hint1: 'There is a combinator to target all
    children of the div', 13 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator', 14 | solution: 'div > *', 15 | } 16 | -------------------------------------------------------------------------------- /src/js/puzzles/level4.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 | 3 | 4 |
    5 | 6 | 7 | 8 |
    9 |
    ` 10 | 11 | export default { 12 | code, 13 | goal: [false, true, false, false, false, true, false, false, false], 14 | hint1: 'You can target by attributes if
    you add them in square brackets', 15 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors', 16 | solution: 'span[data-item]', 17 | } 18 | -------------------------------------------------------------------------------- /src/js/puzzles/level5.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 | 3 | 4 | 5 |

    6 | 7 | 8 |

    9 | 10 | 11 |

    12 |
    ` 13 | 14 | export default { 15 | code, 16 | goal: [false, false, false, false, false, true, true, false, false, true, false, false], 17 | hint1: 'There is a combinator that
    matches following elements', 18 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator', 19 | solution: 'p ~ span', 20 | } 21 | -------------------------------------------------------------------------------- /src/js/puzzles/level6.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 | 3 | 4 | 5 | 6 | 7 | 8 |
    ` 9 | 10 | export default { 11 | code, 12 | goal: [false, true, false, true, true, false, true, false], 13 | hint1: 'You can use a pseudo-class to target
    elements with a specific state', 14 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/:enabled', 15 | solution: ':enabled', 16 | } 17 | -------------------------------------------------------------------------------- /src/js/puzzles/level7.js: -------------------------------------------------------------------------------- 1 | const code = `
      2 |
    1. 3 |
    2. 4 |
    3. 5 |
    4. 6 |
    5. 7 |
    6. 8 |
    7. 9 |
    8. 10 |
    9. 11 |
    10. 12 |
    ` 13 | 14 | export default { 15 | code, 16 | goal: [false, true, true, false, false, true, true, false, false, true, false, false], 17 | hint1: 'Looks stupid - but try to use
    the id selector for this one', 18 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/Selector_list', 19 | solution: '#one, #two, #five, #six, #nine', 20 | } 21 | -------------------------------------------------------------------------------- /src/js/puzzles/level8.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 | 3 |

    4 | 5 | 6 |

    7 |

    8 | 9 | 10 | 11 | 12 |

    13 | 14 | 15 |
    ` 16 | 17 | export default { 18 | code, 19 | goal: [false, false, false, false, true, false, false, false, false, true, false, false, false, true], 20 | hint1: 'Use a combinator that targets the immediate next child', 21 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator', 22 | solution: 'a + span', 23 | } 24 | -------------------------------------------------------------------------------- /src/js/puzzles/level9.js: -------------------------------------------------------------------------------- 1 | const code = `
    2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    ` 10 | 11 | export default { 12 | code, 13 | goal: [false, true, false, false, false, false, false, true, false], 14 | hint1: 'Here, the direct child selector
    is helpful again', 15 | hint2: 'https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator', 16 | solution: '#foo > .foo', 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | position: absolute; 3 | bottom: 0; 4 | left: 0; 5 | width: 100vw; 6 | 7 | display: flex; 8 | justify-content: space-between; 9 | background: var(--nc-bg-2); 10 | border-top: 1px solid var(--nc-bg-3); 11 | padding: 0.75rem 1.5rem; 12 | padding-right: 1.5rem; 13 | padding-left: 1.5rem; 14 | margin: 0 calc(0px - (50vw - 50%)); 15 | padding-left: calc(50vw - 50% + 2rem); 16 | padding-right: calc(50vw - 50% + 2rem); 17 | 18 | @media (max-width: 600px) { 19 | font-size: 0.8rem; 20 | } 21 | } 22 | 23 | .footer-right { 24 | display: flex; 25 | flex-direction: column; 26 | align-items: flex-end; 27 | 28 | a { 29 | color: #fff; 30 | opacity: 0.45; 31 | 32 | &:hover { 33 | opacity: 1; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/scss/_header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | 6 | > a { 7 | display: flex; 8 | align-items: center; 9 | color: #fff; 10 | 11 | &:hover { 12 | color: #fff; 13 | } 14 | } 15 | } 16 | 17 | header svg { 18 | margin-right: 24px; 19 | width: 50px; 20 | height: 50px; 21 | } 22 | 23 | header h1 { 24 | font-family: 'ZCOOL KuaiLe', Verdana, Geneva, Tahoma, sans-serif; 25 | padding: 0; 26 | margin: 0; 27 | 28 | @media (max-width: 600px) { 29 | font-size: 1.5rem; 30 | } 31 | } 32 | 33 | header .social { 34 | display: flex; 35 | align-items: center; 36 | 37 | a { 38 | color: #fff; 39 | opacity: 0.45; 40 | display: inline-flex; 41 | 42 | &:hover { 43 | opacity: 1; 44 | } 45 | } 46 | svg { 47 | width: 36px; 48 | height: 36px; 49 | margin: 0 0 0 24px; 50 | } 51 | span { 52 | display: none; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/scss/_main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | position: relative; 3 | min-height: 100vh; 4 | padding-bottom: 100px; 5 | } 6 | 7 | .hidden { 8 | display: none; 9 | } 10 | 11 | a, button { 12 | cursor: pointer; 13 | } 14 | 15 | button:disabled { 16 | cursor: default; 17 | } 18 | 19 | details { 20 | list-style: disclosure-closed; 21 | padding-left: 25px; 22 | 23 | summary { 24 | padding-left: 5px; 25 | } 26 | } 27 | 28 | details[open] { 29 | list-style: disclosure-open; 30 | } 31 | 32 | main section { 33 | width: 100%; 34 | min-height: 300px; 35 | display: flex; 36 | 37 | @media (max-width: 600px) { 38 | flex-direction: column; 39 | } 40 | } 41 | 42 | .left-column { 43 | width: 60%; 44 | margin-right: 10px; 45 | 46 | @media (max-width: 600px) { 47 | width: 100%; 48 | margin: 10px 0; 49 | } 50 | } 51 | 52 | .right-column { 53 | width: 40%; 54 | margin-left: 10px; 55 | 56 | @media (max-width: 600px) { 57 | width: 100%; 58 | margin: 10px 0; 59 | } 60 | } 61 | 62 | .codebox { 63 | position: relative; 64 | display: flex; 65 | 66 | pre:first-child { overflow: hidden; } 67 | pre:last-child { width: 100%; } 68 | } 69 | 70 | #css-input { 71 | width: calc(100% - 50px); 72 | margin-right: 5px; 73 | 74 | &.error { 75 | border-color: #B71C1C; 76 | } 77 | 78 | &.success { 79 | border-color: #43A047; 80 | } 81 | } 82 | 83 | @keyframes timerSuccess { 84 | 0% { opacity: 0; transform: translateY(0); } 85 | 80% { opacity: 1; transform: translateY(-100%); } 86 | 100% { opacity: 0; transform: translateY(-100%); } 87 | } 88 | 89 | 90 | #timer { 91 | display: block; 92 | position: relative; 93 | 94 | font-size: 23px; 95 | width: 100%; 96 | text-align: center; 97 | margin-bottom: 14px; 98 | 99 | &.done { 100 | background: #2E7D32; 101 | } 102 | 103 | &::before { 104 | content: attr(data-before); 105 | position: absolute; 106 | opacity: 0; 107 | color: #43A047; 108 | visibility: hidden; 109 | } 110 | 111 | &.success::before { 112 | visibility: visible; 113 | animation-name: timerSuccess; 114 | animation-duration: 1.5s; 115 | } 116 | } 117 | 118 | #levels { 119 | padding-left: 0; 120 | 121 | > li { 122 | position: relative; 123 | display: flex; 124 | align-items: center; 125 | 126 | &::before { 127 | content: url('data:image/svg+xml;charset=UTF-8, '); 128 | background-size: 20px 20px; 129 | padding: 0 10px; 130 | display: inline-flex; 131 | } 132 | } 133 | 134 | .active { 135 | font-weight: 900; 136 | } 137 | 138 | .done { 139 | color: #43A047; 140 | 141 | &::before { 142 | content: url('data:image/svg+xml;charset=UTF-8, '); 143 | background-size: 20px 20px; 144 | } 145 | } 146 | 147 | .timeResult { 148 | color: #fff; 149 | margin: 0 0 0 auto; 150 | font-style: italic; 151 | opacity: 0.5; 152 | } 153 | } 154 | 155 | #share-tweet { 156 | display: inline-flex; 157 | align-items: center; 158 | border: 2px solid var(--nc-bg-3); 159 | padding: 6px 10px; 160 | border-radius: 4px; 161 | margin-top: 1rem; 162 | 163 | svg { 164 | margin-right: 6px; 165 | } 166 | } 167 | 168 | #result-screen { 169 | h1 { 170 | padding-top: 0; 171 | padding-bottom: 1rem; 172 | } 173 | } 174 | 175 | #hint1 { 176 | margin-right: 20px; 177 | } 178 | 179 | #hint1, #hint2 { 180 | color: #fff; 181 | opacity: 0; 182 | display: inline-flex; 183 | align-items: center; 184 | transition: opacity ease-in-out 0.5s; 185 | visibility: hidden; 186 | 187 | &.fade-in { 188 | visibility: visible; 189 | opacity: 0.45; 190 | } 191 | 192 | > svg { 193 | width: 20px; 194 | height: 20px; 195 | margin-right: 5px; 196 | } 197 | 198 | &:hover { 199 | opacity: 1; 200 | } 201 | } 202 | 203 | #solution, #next-level { 204 | margin: 0; 205 | } 206 | 207 | #next-level { 208 | opacity: 0.5; 209 | } 210 | 211 | #html-selection { 212 | position: absolute; 213 | width: 100%; 214 | height: 100%; 215 | } 216 | 217 | #html-preview { 218 | position: relative; 219 | display: block; 220 | 221 | .check { 222 | position: absolute; 223 | top: 0; 224 | left: 0; 225 | width: 100%; 226 | height: 100%; 227 | padding: 0; 228 | margin: 0; 229 | 230 | li { 231 | width: 100%; 232 | opacity: 0.2; 233 | height: 1.5em; 234 | margin: 0; 235 | } 236 | 237 | .correct { 238 | background-color: #66BB6A; 239 | } 240 | .wrong { 241 | background-color: #EF5350; 242 | } 243 | } 244 | } 245 | 246 | #tooltip { 247 | background: #333; 248 | color: white; 249 | font-weight: bold; 250 | padding: 4px 8px; 251 | font-size: 1em; 252 | border-radius: 4px; 253 | display: none; 254 | z-index: 1; 255 | } 256 | 257 | #tooltip[data-show] { 258 | display: block; 259 | } 260 | -------------------------------------------------------------------------------- /src/scss/_reset.scss: -------------------------------------------------------------------------------- 1 | /*** The new CSS Reset - version 1.4.4 (last updated 22.12.2021) ***/ 2 | 3 | /* 4 | Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property 5 | - The "symbol *" part is to solve Firefox SVG sprite bug 6 | */ 7 | *:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) { 8 | all: unset; 9 | display: revert; 10 | } 11 | 12 | /* Preferred box-sizing value */ 13 | *, 14 | *::before, 15 | *::after { 16 | box-sizing: border-box; 17 | } 18 | 19 | /* Remove list styles (bullets/numbers) */ 20 | ol, ul, menu { 21 | list-style: none; 22 | } 23 | 24 | /* For images to not be able to exceed their container */ 25 | img { 26 | max-width: 100%; 27 | } 28 | 29 | /* removes spacing between cells in tables */ 30 | table { 31 | border-collapse: collapse; 32 | } 33 | 34 | /* revert the 'white-space' property for textarea elements on Safari */ 35 | textarea { 36 | white-space: revert; 37 | } 38 | 39 | /* fix the feature of 'hidden' attribute. 40 | display:revert; revert to element instead of attribute */ 41 | :where([hidden]) { 42 | display: none; 43 | } 44 | 45 | /* revert for bug in Chromium browsers 46 | - fix for the content editable attribute will work properly. */ 47 | :where([contenteditable]) { 48 | -moz-user-modify: read-write; 49 | -webkit-user-modify: read-write; 50 | overflow-wrap: break-word; 51 | -webkit-line-break: after-white-space; 52 | } 53 | 54 | /* apply back the draggable feature - exist only in Chromium and Safari */ 55 | :where([draggable="true"]) { 56 | -webkit-user-drag: element; 57 | } -------------------------------------------------------------------------------- /src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import 'reset.scss'; 2 | @import 'header.scss'; 3 | @import 'main.scss'; 4 | @import 'footer.scss'; -------------------------------------------------------------------------------- /src/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CSS Speedrun | Test your CSS Skills 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
    38 | 39 | 40 |

    CSS Speedrun

    41 |
    42 |
    43 | 49 |
    50 |
    51 | 52 |
    53 |
    54 | How does it work? 55 |

    Enter the CSS selector, which applies to all elements with an arrow ➡️ and press enter or the submit button.

    56 |

    The timer will start once you correctly solved the first puzzle.

    57 |
    58 | 59 |
    60 |
    61 | 76 | 77 | 78 | 79 | 80 | 84 | 85 |
    86 |
    87 |
    88 |
    89 |
    90 | Testy 91 |
    92 | 93 | 94 | Hint 1 95 | 96 | 97 | 98 | Hint 2 99 | 100 |
    101 | 102 |
    103 | 00:00:0 104 |
    105 |
      106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 | 113 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/views/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CSS Speedrun | Privacy Policy 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
    23 | 24 | 25 |

    CSS Speedrun

    26 |
    27 |
    28 | 34 |
    35 |
    36 | 37 |
    38 |

    Privacy Policy

    39 | 40 |

    Honest and human readable version

    41 |

    42 | I try to keep this website as privacy friendly as possible. 43 | I am using a self hosted version of umami for the only purpose of knowing how many people are visiting this website. 44 |

    45 |

    46 | If you enable "Do Not Track" in your browser I won't track you. 47 | Also AdBlockers will work for blocking the tracking. I'd recomment uBlock Origin for that. 48 |

    49 |

    50 | Other than that there is nothing that could track you, as far as I'm aware of. No share links (eg. facebook), 51 | no cookies and no other tools. 52 |

    53 |

    54 | If you think I'm missing something here - please let me know: info@wweb.dev 55 |

    56 | 57 |
    58 |
    59 |
    60 | 61 |

    Version for my legal safety

    62 |

    Effective date: January 15, 2020

    63 |

    Updated date: May 07 2020

    64 |

    65 | Vincent Will ('us', 'we', or 'our') operates the 66 | https://wweb.dev website (hereinafter referred to 67 | as the 'Service'). 68 |

    69 |

    70 | This page informs you of our policies regarding the 71 | collection, use, and disclosure of personal data when 72 | you use our Service and the choices you have associated 73 | with that data. Our Privacy Policy for Vincent Will is 74 | created with the help of the 75 | 76 | PrivacyPolicies.com Privacy Policy Generator 77 | . 78 |

    79 |

    80 | We use your data to provide and improve the Service. By 81 | using the Service, you agree to the collection and use 82 | of information in accordance with this policy. Unless 83 | otherwise defined in this Privacy Policy, the terms used 84 | in this Privacy Policy have the same meanings as in our 85 | Terms and Conditions, accessible from 86 | https://wweb.dev 87 |

    88 |

    Information Collection And Use

    89 |

    90 | We collect several different types of information for 91 | various purposes to provide and improve our Service to 92 | you. 93 |

    94 |

    Types of Data Collected

    95 |

    Personal Data

    96 |

    97 | While using our Service, we may ask you to provide us 98 | with certain personally identifiable information that 99 | can be used to contact or identify you ('Personal 100 | Data'). Personally identifiable information may include, 101 | but is not limited to: 102 |

    103 | 106 |

    Usage Data

    107 |

    108 | We may also collect information on how the Service is 109 | accessed and used ('Usage Data'). This Usage Data may 110 | include information such as your computer's Internet 111 | Protocol address (e.g. IP address), browser type, 112 | browser version, the pages of our Service that you 113 | visit, the time and date of your visit, the time spent 114 | on those pages, unique device identifiers and other 115 | diagnostic data. 116 |

    117 |

    Tracking & Cookies Data

    118 |

    119 | We use cookies and similar tracking technologies to 120 | track the activity on our Service and hold certain 121 | information. 122 |

    123 |

    124 | Cookies are files with small amount of data which may 125 | include an anonymous unique identifier. Cookies are sent 126 | to your browser from a website and stored on your 127 | device. Tracking technologies also used are beacons, 128 | tags, and scripts to collect and track information and 129 | to improve and analyze our Service. 130 |

    131 |

    132 | You can instruct your browser to refuse all cookies or 133 | to indicate when a cookie is being sent. However, if you 134 | do not accept cookies, you may not be able to use some 135 | portions of our Service. You can learn more how to 136 | manage cookies in the 137 | Browser Cookies Guide. 140 |

    141 |

    Examples of Cookies we use:

    142 | 157 |

    Use of Data

    158 |

    159 | Vincent Will uses the collected data for various 160 | purposes: 161 |

    162 | 177 |

    Transfer Of Data

    178 |

    179 | Your information, including Personal Data, may be 180 | transferred to — and maintained on — computers located 181 | outside of your state, province, country or other 182 | governmental jurisdiction where the data protection laws 183 | may differ than those from your jurisdiction. 184 |

    185 |

    186 | If you are located outside Germany and choose to provide 187 | information to us, please note that we transfer the 188 | data, including Personal Data, to Germany and process it 189 | there. 190 |

    191 |

    192 | Your consent to this Privacy Policy followed by your 193 | submission of such information represents your agreement 194 | to that transfer. 195 |

    196 |

    197 | Vincent Will will take all steps reasonably necessary to 198 | ensure that your data is treated securely and in 199 | accordance with this Privacy Policy and no transfer of 200 | your Personal Data will take place to an organization or 201 | a country unless there are adequate controls in place 202 | including the security of your data and other personal 203 | information. 204 |

    205 |

    Disclosure Of Data

    206 |

    Legal Requirements

    207 |

    208 | Vincent Will may disclose your Personal Data in the good 209 | faith belief that such action is necessary to: 210 |

    211 | 227 |

    Security Of Data

    228 |

    229 | The security of your data is important to us, but 230 | remember that no method of transmission over the 231 | Internet, or method of electronic storage is 100% 232 | secure. While we strive to use commercially acceptable 233 | means to protect your Personal Data, we cannot guarantee 234 | its absolute security. 235 |

    236 |

    Service Providers

    237 |

    238 | We may employ third party companies and individuals to 239 | facilitate our Service ('Service Providers'), to provide 240 | the Service on our behalf, to perform Service-related 241 | services or to assist us in analyzing how our Service is 242 | used. 243 |

    244 |

    245 | These third parties have access to your Personal Data 246 | only to perform these tasks on our behalf and are 247 | obligated not to disclose or use it for any other 248 | purpose. 249 |

    250 |

    Analytics

    251 |

    252 | We may use third-party Service Providers to monitor and 253 | analyze the use of our Service. 254 |

    255 | 264 | 265 |

    Links To Other Sites

    266 |

    267 | Our Service may contain links to other sites that are 268 | not operated by us. If you click on a third party link, 269 | you will be directed to that third party's site. We 270 | strongly advise you to review the Privacy Policy of 271 | every site you visit. 272 |

    273 |

    274 | We have no control over and assume no responsibility for 275 | the content, privacy policies or practices of any third 276 | party sites or services. 277 |

    278 |

    Children's Privacy

    279 |

    280 | Our Service does not address anyone under the age of 18 281 | ('Children'). 282 |

    283 |

    284 | We do not knowingly collect personally identifiable 285 | information from anyone under the age of 18. If you are 286 | a parent or guardian and you are aware that your 287 | Children has provided us with Personal Data, please 288 | contact us. If we become aware that we have collected 289 | Personal Data from children without verification of 290 | parental consent, we take steps to remove that 291 | information from our servers. 292 |

    293 |

    Changes To This Privacy Policy

    294 |

    295 | We may update our Privacy Policy from time to time. We 296 | will notify you of any changes by posting the new 297 | Privacy Policy on this page. 298 |

    299 |

    300 | We will let you know via email and/or a prominent notice 301 | on our Service, prior to the change becoming effective 302 | and update the 'effective date' at the top of this 303 | Privacy Policy. 304 |

    305 |

    306 | You are advised to review this Privacy Policy 307 | periodically for any changes. Changes to this Privacy 308 | Policy are effective when they are posted on this page. 309 |

    310 |

    Contact Us

    311 |

    312 | If you have any questions about this Privacy Policy, 313 | please contact us: 314 |

    315 | 318 |
    319 | 320 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/js/main.js', 3 | output: { 4 | path: __dirname + '/dist', 5 | filename: 'bundle.js', 6 | }, 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.m?js$/, 11 | exclude: /node_modules/, 12 | use: { 13 | loader: 'babel-loader', 14 | options: { 15 | presets: ['@babel/preset-env'] 16 | } 17 | } 18 | } 19 | ] 20 | } 21 | } --------------------------------------------------------------------------------