├── .eleventy.js ├── .github └── pull_request_template.md ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── api └── user-image.js ├── lib ├── slug.js └── teachers.js ├── now.json ├── package-lock.json ├── package.json ├── screenshot.png ├── scripts ├── add-teacher.js ├── create-feeds.js └── create-screenshots.js ├── site ├── _data │ ├── contributors.js │ ├── styles.js │ ├── tags.js │ └── vars.js ├── _includes │ ├── base.njk │ ├── css │ │ ├── _base.css │ │ ├── _fonts.css │ │ ├── components │ │ │ ├── _buttons.css │ │ │ ├── _carbon.css │ │ │ ├── _card.css │ │ │ ├── _nav.css │ │ │ └── _skip-link.css │ │ ├── index.css │ │ └── util │ │ │ ├── _colors.css │ │ │ ├── _dimensions.css │ │ │ ├── _flex.css │ │ │ ├── _fonts.css │ │ │ ├── _grid.css │ │ │ ├── _margins.css │ │ │ ├── _paddings.css │ │ │ ├── _position.css │ │ │ ├── _shadows.css │ │ │ └── _visibility.css │ ├── js │ │ └── main.js │ └── templates │ │ ├── _list.njk │ │ ├── _logo.njk │ │ ├── _navigation.njk │ │ └── _social.njk ├── index.html ├── tag-sorted-by-date.njk └── tag.njk ├── static ├── apple-touch-icon.png ├── exo.woff ├── exo.woff2 ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── large-media-image.jpg └── paid.svg └── teachers ├── a11yphant.json ├── can't-unsee.json ├── css-animation-course.json ├── css-diner.json ├── css-for-js-developers.json ├── design-patterns-game.json ├── designercize.json ├── divize.json ├── flexbox-adventures.json ├── flexbox-defense.json ├── flexbox-froggy.json ├── future-coder.json ├── grid-garden.json ├── jsrobot.json ├── knights-of-the-flexbox-table.json ├── learn-git-branching.json ├── learn-html-css.json ├── mess-with-dns.json ├── open-vim.json ├── react-hooks-cheatsheet.json ├── react-tutorial.json ├── regex-crossword.json ├── regexlearn.json ├── regexone.json ├── select-star-sql.json ├── sqlbolt.json ├── the-bezier-game.json ├── the-boolean-game.json ├── the-font-game.json ├── typescript-exercises.json ├── typewar.json └── web.dev-"learn-css".json /.eleventy.js: -------------------------------------------------------------------------------- 1 | const htmlmin = require('html-minifier'); 2 | const Terser = require('terser'); 3 | const { NODE_ENV } = process.env; 4 | 5 | module.exports = function (eleventyConfig) { 6 | eleventyConfig.addPassthroughCopy({ static: '.' }); 7 | 8 | eleventyConfig.addFilter('jsmin', function (code) { 9 | let minified = Terser.minify(code); 10 | if (minified.error) { 11 | console.log('Terser error: ', minified.error); 12 | return code; 13 | } 14 | 15 | return minified.code; 16 | }); 17 | 18 | eleventyConfig.addTransform('htmlmin', function (content, outputPath) { 19 | if (outputPath.endsWith('.html')) { 20 | let minified = htmlmin.minify(content, { 21 | useShortDoctype: true, 22 | removeComments: true, 23 | collapseWhitespace: true, 24 | }); 25 | return minified; 26 | } 27 | 28 | return content; 29 | }); 30 | 31 | eleventyConfig.addNunjucksFilter('route', function ( 32 | slug, 33 | { listIsSortedBy } 34 | ) { 35 | // in production the home dir is mapped to root 36 | if (NODE_ENV === 'production' && slug === 'home') { 37 | slug = ''; 38 | } 39 | 40 | let route = slug.length ? `/${slug}/` : '/'; 41 | 42 | if (listIsSortedBy === 'addedAt') { 43 | route += 'latest/'; 44 | } 45 | 46 | return route; 47 | }); 48 | 49 | eleventyConfig.addNunjucksFilter('prettyDate', function (dateString) { 50 | const date = new Date(dateString); 51 | const regex = /^(?\w+?)\s(?\w+?)\s(?\w+?) (?\d+?)$/; 52 | const matched = date.toDateString().match(regex); 53 | 54 | if (matched) { 55 | const { month, year } = matched.groups; 56 | return `${month} ${year}`; 57 | } 58 | 59 | return dateString; 60 | }); 61 | 62 | return { 63 | dir: { 64 | input: 'site', 65 | }, 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Thank you for opening a pull request to tiny-teachers.dev ❤️ 2 | 3 | Before you submit your PR, please take a look at the [contributing guidelines](https://github.com/stefanjudis/tiny-teachers/blob/master/CONTRIBUTING.md). :) 4 | 5 | To make it easier for us to review and merge your PR, please make sure … 6 | 7 | - [ ] you only add one (!) new teacher per pull request. 8 | - [ ] you have checked if an open PR already exists. 9 | - [ ] the submitted website is focused on a single, development related issue. 10 | - [ ] the `desc` field includes an "actionable sentence" (e.g. "Learn about CSS flexbox" or "Learn how to design well."). 11 | 12 | Thank you! 🙇‍♂️ 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | yarn.lock 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # generated stuff 110 | _site 111 | site/_includes/main.* 112 | static/screenshots 113 | static/*.js 114 | static/*.xml 115 | static/*.html 116 | TODO.txt 117 | 118 | #WebStorm IDE 119 | .idea 120 | .now 121 | .vercel -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | teachers/*.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": true } 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Criteria to add a new tiny teacher 2 | 3 | - ❗ **Please only add one (!) new teacher per pull request.** This will speed up the review and merge process. 4 | 5 | - ❗ **Please have a look at open PRs and issues.** There might be the chance that someone else opened a PR with your tool already. :) 6 | 7 | ## What does count as a tiny teacher? 8 | 9 | > A collection of online learning tools and courses for web developers. 10 | 11 | - A tiny teacher is any website or web application that educates developers in a playful way. 12 | - A tiny teacher does not have to be available on GitHub and has not to be open source. 13 | - A tiny teacher can be used right away. 14 | - **It has to be useful, that's all.** 🎉 15 | 16 | ## What does not(!) count as a tiny helper? 17 | 18 | - **JS or CSS libraries / npm modules** (tiny-teachers.dev is about online resources) 19 | 20 | ## Formatting of tiny teachers 21 | 22 | Your generated helper JSON files have to follow these criterias: 23 | 24 | - `desc` - includes an "actionable sentence" 25 | 26 | ✅ DO: "Learn about CSS flexbox" 27 | 28 | ❌ DON'T: "ABC is a tool that teaches flexbox" 29 | 30 | - `maintainers` - includes a human being (and not companies) 31 | 32 | ✅ DO: ["individualA", "individualB"] 33 | 34 | ❌ DON'T: ["companyA"] 35 | 36 | _It's okay if the helper is closed source and source code is not available on GitHub._ 37 | 38 | - `tags` - includes tags provided by the `npm run helper:add` cli command 39 | 40 | ✅ DO: ["Accessibility", "Color"] 41 | 42 | ❌ DON'T: ["Some new tag"] 43 | 44 | _Please don't just create some new tags, we want to be careful to not introduce tags that will only include one helper._ 45 | 46 | _Please don't set more than three tags, we want to keep the tags tidy._ 47 | 48 | --- 49 | 50 | To sum it up – your JSON addition should look as follows: 51 | 52 | ```json 53 | { 54 | "name": "A new teacher", 55 | "desc": "Learn about CSS flexbox.", 56 | "url": "https://some.url", 57 | "tags": ["Misc"], 58 | "maintainers": ["PersonA"], 59 | "addedAt": "2020-01-17" 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stefan Judis 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 | # tiny-teachers.dev 2 | 3 | > A collection of online learning tools and courses for web developers. 4 | 5 | ![Screenshot of tiny-helpers.dev](./screenshot.png) 6 | 7 | ## Contributing 8 | 9 | Make sure you have a recent version of [Node.js installed](https://nodejs.org/en/) (we recommend at least version `v12.14.`). After installing Node.js you'll have the `node` but also the [`npm`](https://www.npmjs.com/) command available. npm is Node.js' package manager. 10 | 11 | **Additionally, please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) including further information about what counts as a tiny helper.** 12 | 13 | Fork and clone this repository. Head over to your terminal and run the following command: 14 | 15 | ``` 16 | git clone git@github.com:[YOUR_USERNAME]/tiny-teachers.git 17 | cd tiny-teachers 18 | npm ci 19 | npm run teacher:add 20 | ``` 21 | 22 | ### Add a new teacher 23 | 24 | `npm run teacher:add` will ask a few questions and create a file in `helpers/`. 25 | Commit the changes and [open a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). 26 | 27 | ### Run the project locally 28 | 29 | This project uses Vercel's routing configuration. The `/` route doesn't work locally. To start, navigate to `localhost:8080/home/` after running `npm run dev`. 30 | 31 | ``` 32 | npm run dev 33 | ``` 34 | -------------------------------------------------------------------------------- /api/user-image.js: -------------------------------------------------------------------------------- 1 | const got = require('got'); 2 | 3 | module.exports = async (req, res) => { 4 | try { 5 | const { user, size } = req.query; 6 | const GITHUB_URL = `https://github.com/${user}.png${ 7 | size ? `?size=${size}` : '' 8 | }`; 9 | const imageRequest = got(GITHUB_URL); 10 | 11 | const [imageResponse, imageBuffer] = await Promise.all([ 12 | imageRequest, 13 | imageRequest.buffer(), 14 | ]); 15 | 16 | res.setHeader('Cache-Control', 's-maxage=43200'); 17 | res.setHeader('content-type', imageResponse.headers['content-type']); 18 | res.send(imageBuffer); 19 | } catch (error) { 20 | if (error.message.includes('404')) { 21 | res.status(404); 22 | return res.send('Not found'); 23 | } 24 | 25 | res.status(500); 26 | res.send(error.message); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/slug.js: -------------------------------------------------------------------------------- 1 | const slugify = require('slugify'); 2 | 3 | const toSlug = name => slugify(name).toLocaleLowerCase(); 4 | 5 | module.exports.toSlug = toSlug; 6 | -------------------------------------------------------------------------------- /lib/teachers.js: -------------------------------------------------------------------------------- 1 | const { readFile, readdir, writeFile } = require('fs').promises; 2 | const { toSlug } = require('./slug'); 3 | const path = require('path'); 4 | 5 | const teachersDir = path.resolve(__dirname, '..', 'teachers'); 6 | 7 | async function getTeachers() { 8 | const teachers = Object.values({ 9 | ...(await getTeachersFromFiles()), 10 | }); 11 | teachers.sort((a, b) => 12 | a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1 13 | ); 14 | return teachers; 15 | } 16 | 17 | async function getTeachersFromFiles() { 18 | const files = (await readdir(teachersDir)).filter((name) => 19 | name.endsWith('.json') 20 | ); 21 | const texts = await Promise.all( 22 | files.map((name) => readFile(path.join(teachersDir, name))) 23 | ); 24 | const helpers = texts.map((text) => JSON.parse(text)); 25 | return Object.fromEntries(helpers.map((h) => [h.name, h])); 26 | } 27 | 28 | async function writeTeacher(teacher) { 29 | const filePath = path.join(teachersDir, `${toSlug(teacher.name)}.json`); 30 | const data = JSON.stringify(teacher, null, 2) + '\n'; 31 | await writeFile(filePath, data); 32 | return filePath; 33 | } 34 | 35 | function getTags(teachers) { 36 | return [ 37 | ...teachers.reduce((acc, cur) => { 38 | cur.tags.forEach((tag) => acc.add(tag)); 39 | return acc; 40 | }, new Set()), 41 | ].sort((a, b) => (a < b ? -1 : 1)); 42 | } 43 | 44 | module.exports.getTags = getTags; 45 | module.exports.getTeachers = getTeachers; 46 | module.exports.teachersDir = teachersDir; 47 | module.exports.writeTeacher = writeTeacher; 48 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "src": "/", 5 | "dest": "/home/index.html" 6 | }, 7 | { 8 | "src": "/latest/", 9 | "dest": "/home/latest/index.html" 10 | } 11 | ], 12 | "build": {} 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-teachers", 3 | "version": "1.0.0", 4 | "description": "A collection of online learning tools and courses for web developers.", 5 | "main": "index.js", 6 | "scripts": { 7 | "setup:screenshots": "node ./scripts/create-screenshots.js", 8 | "setup:feeds": "node ./scripts/create-feeds.js", 9 | "setup": "concurrently \"npm:setup:*\"", 10 | "teacher:add": "node ./scripts/add-teacher.js", 11 | "dev": "eleventy --serve --port $PORT", 12 | "dev:vercel": "vercel dev", 13 | "prebuild": "npm run setup", 14 | "build": "eleventy --input=./site", 15 | "deploy": "npm run build && now --prod" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/stefanjudis/tiny-teachers.git" 20 | }, 21 | "engines": { 22 | "node": "18.x" 23 | }, 24 | "keywords": [], 25 | "author": "stefan judis ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/stefanjudis/tiny-teachers/issues" 29 | }, 30 | "homepage": "https://github.com/stefanjudis/tiny-teachers#readme", 31 | "dependencies": { 32 | "@11ty/eleventy": "^0.10.0", 33 | "chrome-aws-lambda": "^2.1.1", 34 | "concurrently": "^5.1.0", 35 | "copy-dir": "^1.3.0", 36 | "feed": "^4.1.0", 37 | "got": "^10.7.0", 38 | "html-minifier": "^4.0.0", 39 | "inquirer": "^7.1.0", 40 | "jimp": "^0.9.5", 41 | "now": "^17.1.1", 42 | "p-limit": "^2.2.2", 43 | "postcss": "^7.0.27", 44 | "postcss-clean": "^1.1.0", 45 | "postcss-import": "^12.0.1", 46 | "postcss-nested": "^4.2.1", 47 | "puppeteer": "^2.1.1", 48 | "puppeteer-core": "^2.1.1", 49 | "slugify": "^1.4.0", 50 | "terser": "^4.6.7" 51 | }, 52 | "devDependencies": { 53 | "vercel": "^19.1.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/screenshot.png -------------------------------------------------------------------------------- /scripts/add-teacher.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const { getTeachers, writeTeacher, getTags } = require('../lib/teachers'); 3 | 4 | (async () => { 5 | try { 6 | const tags = getTags(await getTeachers()); 7 | 8 | console.log('Thank you for contributing to tiny-teachers.dev!\n'); 9 | console.log( 10 | 'Let me give you some guidance and tips on how to add a "good teacher":\n' 11 | ); 12 | console.log( 13 | '✅ `desc` – DO: "Learn about CSS flexbox." or "Learn how to design well."' 14 | ); 15 | console.log( 16 | '❌ `desc` – DON\'T: "ABC is a tool that can something great"\n' 17 | ); 18 | console.log('✅ `maintainers` – DO: individualA,individualB'); 19 | console.log("❌ `maintainers` – DON'T: companyA\n"); 20 | 21 | const newTeacher = await inquirer.prompt([ 22 | { 23 | name: 'name', 24 | type: 'input', 25 | message: 'Enter the name of the teacher:', 26 | }, 27 | { name: 'desc', type: 'input', message: 'Enter a description:' }, 28 | { name: 'url', type: 'input', message: 'Enter a URL:' }, 29 | { 30 | name: 'tags', 31 | type: 'checkbox', 32 | message: 'Pick some tags:', 33 | choices: tags, 34 | }, 35 | { 36 | name: 'maintainers', 37 | type: 'input', 38 | message: 39 | 'Enter GitHub handles of the tool maintainers (comma separated):', 40 | }, 41 | { 42 | name: 'loginRequired', 43 | type: 'confirm', 44 | message: 'Does this teacher require to create an account?', 45 | }, 46 | { 47 | name: 'isPaid', 48 | type: 'confirm', 49 | message: 'Do you have to pay for it?', 50 | }, 51 | ]); 52 | 53 | if (!newTeacher.tags || !newTeacher.tags.length) { 54 | throw new Error( 55 | 'Please define at least one tag for your teacher.\nIf no tag fits your teacher please open issue to add a new tag.\n👉 https://github.com/stefanjudis/tiny-teachers/issues/new' 56 | ); 57 | } 58 | 59 | newTeacher.addedAt = new Date().toISOString().substring(0, 10); 60 | newTeacher.maintainers = newTeacher.maintainers.length 61 | ? newTeacher.maintainers.split(',') 62 | : []; 63 | 64 | const filePath = await writeTeacher(newTeacher); 65 | 66 | console.log(`Thanks!!! ${filePath} was created!`); 67 | } catch (error) { 68 | console.error(error); 69 | } 70 | })(); 71 | -------------------------------------------------------------------------------- /scripts/create-feeds.js: -------------------------------------------------------------------------------- 1 | const Feed = require('feed').Feed; 2 | const { description } = require('../package.json'); 3 | const { getTeachers } = require('../lib/teachers'); 4 | const { writeFile } = require('fs').promises; 5 | const { join } = require('path'); 6 | const { toSlug } = require('../lib/slug'); 7 | 8 | (async () => { 9 | const helpers = await getTeachers(); 10 | try { 11 | const feed = new Feed({ 12 | title: 'Tiny Teachers', 13 | description, 14 | id: 'https://tiny-teachers.dev/', 15 | link: 'https://tiny-teachers.dev/', 16 | language: 'en', 17 | image: 'http://example.com/image.png', 18 | favicon: 'https://tiny-teachers.dev/favicon.ico', 19 | copyright: `All rights reserved ${new Date().getUTCFullYear()}, Stefan Judis`, 20 | generator: 'Feed for tiny-teachers.dev', // optional, default = 'Feed for Node.js' 21 | feedLinks: { 22 | atom: 'https://tiny-teachers.dev/feed.atom', 23 | rss: 'https://tiny-teachers.dev/feed.xml', 24 | }, 25 | author: { 26 | name: 'Stefan Judis', 27 | email: 'stefanjudis@gmail.com', 28 | link: 'https://www.stefanjudis.com', 29 | }, 30 | }); 31 | 32 | helpers 33 | .sort((a, b) => (new Date(a.addedAt) < new Date(b.addedAt) ? 1 : -1)) 34 | .forEach(({ addedAt, name, desc, url }) => { 35 | feed.addItem({ 36 | title: `New teacher added: ${name} – ${desc}.`, 37 | id: toSlug(name), 38 | link: url, 39 | description: desc, 40 | content: `More teachers! 🎉🎉🎉 "${name}" is available at ${url}`, 41 | date: new Date(addedAt), 42 | image: `https://tiny-teachers.dev/screenshots/${toSlug(name)}@1.jpg`, 43 | }); 44 | }); 45 | 46 | console.log('Writing rss feed'); 47 | writeFile(join('.', 'static', 'feed.xml'), feed.rss2()); 48 | } catch (error) { 49 | console.error(error); 50 | process.exit(1); 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /scripts/create-screenshots.js: -------------------------------------------------------------------------------- 1 | const pLimit = require('p-limit'); 2 | const chrome = require('chrome-aws-lambda'); 3 | const { statSync, mkdirSync } = require('fs'); 4 | const { join } = require('path'); 5 | const copyDir = require('copy-dir'); 6 | const Jimp = require('jimp'); 7 | const { getTeachers } = require('../lib/teachers'); 8 | const { toSlug } = require('../lib/slug'); 9 | 10 | function exists(path) { 11 | try { 12 | statSync(path); 13 | return true; 14 | } catch (err) { 15 | if (err.code === 'ENOENT') return false; 16 | throw err; 17 | } 18 | } 19 | 20 | function sleep(duration) { 21 | return new Promise((resolve) => setTimeout(resolve, duration)); 22 | } 23 | 24 | async function screenshotTeacher(browser, teacher, screenshotDir) { 25 | try { 26 | const doubleSize = join(screenshotDir, `${toSlug(teacher.name)}@2.jpg`); 27 | const singleSize = join(screenshotDir, `${toSlug(teacher.name)}@1.jpg`); 28 | let sigil = '✅'; 29 | if (!(await exists(doubleSize))) { 30 | const page = await browser.newPage(); 31 | page.setViewport({ 32 | width: 1000, 33 | height: 600, 34 | }); 35 | await page.goto(teacher.url, { waitUntil: 'networkidle0' }); 36 | 37 | await page.screenshot({ path: doubleSize }); 38 | await page.close(); 39 | sigil = '📸'; 40 | } 41 | if (!exists(singleSize)) { 42 | await (await Jimp.read(doubleSize)) 43 | .quality(75) 44 | .resize(500, Jimp.AUTO) 45 | .write(singleSize); 46 | } 47 | console.log(`${sigil} ${teacher.name} at ${teacher.url}`); 48 | } catch (error) { 49 | console.error(`Failed to screenshot ${teacher.name}`); 50 | throw error; 51 | } 52 | } 53 | 54 | async function makeScreenshots(helpers, { screenshotCacheDir }) { 55 | console.log('Taking screenshots...'); 56 | const browser = await chrome.puppeteer.launch({ 57 | args: chrome.args, 58 | executablePath: await chrome.executablePath, 59 | headless: true, 60 | }); 61 | const limit = pLimit(8); 62 | const screenshotPromises = helpers.map((teacher) => 63 | limit(() => screenshotTeacher(browser, teacher, screenshotCacheDir)) 64 | ); 65 | await Promise.all(screenshotPromises); 66 | await browser.close(); 67 | } 68 | 69 | (async () => { 70 | try { 71 | const screenshotCacheDir = join(__dirname, '..', '.cache', 'screenshots'); 72 | 73 | if (!exists(screenshotCacheDir)) { 74 | mkdirSync(screenshotCacheDir, { recursive: true }); 75 | } 76 | 77 | const helpers = await getTeachers(); 78 | 79 | await makeScreenshots(helpers, { screenshotCacheDir }); 80 | 81 | const publicScreenshotDir = join(__dirname, '..', 'static', 'screenshots'); 82 | copyDir.sync(screenshotCacheDir, publicScreenshotDir); 83 | } catch (error) { 84 | console.error(error); 85 | process.exit(1); 86 | } 87 | })(); 88 | -------------------------------------------------------------------------------- /site/_data/contributors.js: -------------------------------------------------------------------------------- 1 | const got = require('got'); 2 | 3 | const IGNORED_CONTRIBUTORS = ['stefanjudis', 'github-actions[bot]']; 4 | 5 | async function fetchContributors({ page = 1, options }) { 6 | console.log(`Fetching contributors... Page ${page}`); 7 | const response = await got({ 8 | url: `https://api.github.com/repos/stefanjudis/tiny-teachers/contributors?per_page=100&page=${page}`, 9 | ...options, 10 | }); 11 | 12 | const contributors = JSON.parse(response.body) 13 | .map((contributor) => contributor.login) 14 | .filter((contributor) => !IGNORED_CONTRIBUTORS.includes(contributor)); 15 | 16 | const match = 17 | response.headers.link && 18 | response.headers.link.match(/^<.*?&page=(?\d*?)>; rel="next".*$/); 19 | 20 | console.log({ match, contributors }); 21 | return match 22 | ? [ 23 | ...contributors, 24 | ...(await fetchContributors({ page: +match.groups.nextPage, options })), 25 | ] 26 | : contributors; 27 | } 28 | 29 | module.exports = async function () { 30 | // don't hit the API on every rebuild due to rate limits 31 | console.log(process.env.NODE_ENV); 32 | if (process.env.NODE_ENV === 'production') { 33 | try { 34 | const { GITHUB_ACCESS_TOKEN } = process.env; 35 | if (!GITHUB_ACCESS_TOKEN) { 36 | throw new Error('Please define GITHUB_ACCESS_TOKEN'); 37 | } 38 | const options = { 39 | headers: { 40 | authorization: `token ${GITHUB_ACCESS_TOKEN}`, 41 | }, 42 | }; 43 | 44 | return await fetchContributors({ options }); 45 | } catch (e) { 46 | console.log('were erroring'); 47 | console.error(e); 48 | return []; 49 | } 50 | } else { 51 | return ['stefanjudis', 'stefanjudis', 'stefanjudis']; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /site/_data/styles.js: -------------------------------------------------------------------------------- 1 | const { readFile, writeFile } = require('fs').promises; 2 | const path = require('path'); 3 | const postcss = require('postcss'); 4 | 5 | module.exports = async () => { 6 | const rawFilePath = path.join( 7 | __dirname, 8 | '..', 9 | '_includes', 10 | 'css', 11 | 'index.css' 12 | ); 13 | const rawCSS = await readFile(rawFilePath, 'utf8'); 14 | const css = await postcss([ 15 | require('postcss-import'), 16 | require('postcss-nested'), 17 | require('postcss-clean') 18 | ]).process(rawCSS, { from: rawFilePath }); 19 | 20 | return css; 21 | }; 22 | -------------------------------------------------------------------------------- /site/_data/tags.js: -------------------------------------------------------------------------------- 1 | const { toSlug } = require('../../lib/slug'); 2 | const { getTeachers, getTags } = require('../../lib/teachers'); 3 | 4 | module.exports = async function () { 5 | const teacherData = (await getTeachers()).map((helper) => ({ 6 | ...helper, 7 | slug: toSlug(helper.name), 8 | })); 9 | 10 | const tags = getTags(teacherData) 11 | .map((tag) => ({ 12 | name: tag, 13 | title: tag, 14 | slug: `${toSlug(tag)}`, 15 | items: teacherData.filter((teacher) => teacher.tags.includes(tag)), 16 | })) 17 | .sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)); 18 | 19 | const homeTag = { 20 | name: 'All', 21 | title: 'Home', 22 | slug: 'home', 23 | items: teacherData, 24 | }; 25 | 26 | return [homeTag, ...tags]; 27 | }; 28 | -------------------------------------------------------------------------------- /site/_data/vars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isProduction: process.env.NODE_ENV === 'production', 3 | }; 4 | -------------------------------------------------------------------------------- /site/_includes/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | {{ renderData.title }} – Tiny Teachers 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% if renderData.isCanonical %} 34 | {% if tag.slug == 'home' %} 35 | 36 | {% else %} 37 | 38 | {% endif %} 39 | {% endif %} 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | {% include 'templates/_logo.njk' %} 51 | {% include 'templates/_social.njk' %} 52 | {% include 'templates/_navigation.njk' %} 53 | 54 |
55 | {% include 'templates/_list.njk' %} 56 |
57 |
58 | 59 | {% set js %} 60 | {% include "js/main.js" %} 61 | {% endset %} 62 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /site/_includes/css/_base.css: -------------------------------------------------------------------------------- 1 | * { 2 | &, 3 | &::before, 4 | &::after { 5 | box-sizing: border-box; 6 | } 7 | } 8 | 9 | body, 10 | html { 11 | height: 100%; 12 | margin: 0; 13 | padding: 0; 14 | color: var(--text-color); 15 | font-size: 100%; 16 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 17 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | h4 { 24 | font-family: 'Exo', sans-serif; 25 | color: var(--text-highlight-color); 26 | margin: 0; 27 | } 28 | 29 | a { 30 | color: currentColor; 31 | text-decoration: underline; 32 | text-underline-offset: 0.2em; 33 | text-decoration-thickness: 2px; 34 | 35 | &:hover, 36 | &:active { 37 | color: var(--primary-color); 38 | } 39 | } 40 | 41 | svg { 42 | fill: currentColor; 43 | } 44 | 45 | img, 46 | svg { 47 | max-width: 100%; 48 | height: auto; 49 | } 50 | 51 | ul, 52 | ol { 53 | margin: 0; 54 | padding: 0; 55 | list-style: none; 56 | } 57 | 58 | p { 59 | margin: 0 0 1em; 60 | } 61 | 62 | :focus { 63 | outline: 3px solid var(--primary-color); 64 | } 65 | 66 | .js-focus-visible :focus:not(.focus-visible) { 67 | outline: none; 68 | } 69 | -------------------------------------------------------------------------------- /site/_includes/css/_fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Exo'; 3 | font-style: normal; 4 | font-weight: 600; 5 | src: local('Exo SemiBold'), local('Exo-SemiBold'), 6 | url('/exo.woff2') format('woff2'), url('/exo.woff') format('woff'); 7 | font-display: swap; 8 | } 9 | -------------------------------------------------------------------------------- /site/_includes/css/components/_buttons.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | border-radius: 0.25em; 3 | line-height: 1em; 4 | font-family: sans-serif; 5 | font-size: 1em; 6 | margin: 0.25em; 7 | white-space: nowrap; 8 | 9 | svg { 10 | width: auto; 11 | } 12 | } 13 | 14 | .btn--cta { 15 | padding: 0.5em 0.75em; 16 | background: var(--primary-color); 17 | color: #fff; 18 | text-decoration: none; 19 | box-shadow: 0 0.25em 0.75em #308d3482; 20 | display: inline-grid; 21 | grid-template-columns: auto 1em; 22 | grid-gap: 0.75em; 23 | transform: translateY(0); 24 | border: 2px solid transparent; 25 | 26 | &:hover { 27 | background: #3f9f43; 28 | transform: translateY(-1px); 29 | color: #fff; 30 | } 31 | 32 | &:active { 33 | box-shadow: 0 0.125em 0.5em #308d3482; 34 | } 35 | } 36 | 37 | .btn--ghost { 38 | color: var(--primary-color); 39 | padding: 0.75em 0; 40 | display: inline-grid; 41 | grid-template-columns: auto 1.25em; 42 | grid-gap: 0.75em; 43 | transform: translateY(0); 44 | border: 2px solid transparent; 45 | background: transparent; 46 | font-size: 1em; 47 | text-decoration: none; 48 | 49 | &:hover { 50 | text-decoration: underline; 51 | text-underline-offset: 0.2em; 52 | text-decoration-thickness: 0.1em; 53 | } 54 | } 55 | 56 | .btn--full { 57 | display: block; 58 | font-size: 0.875em; 59 | text-align: center; 60 | margin: 1em 2px 2px 2px; 61 | background: var(--sidebar-bg-color); 62 | padding: 0.75em 1em; 63 | color: #fff; 64 | border-top: 1px solid #d9dfe4; 65 | color: var(--primary-color); 66 | } 67 | 68 | .btn--bubble { 69 | font-size: 0.875em; 70 | border: 1px solid var(--light-border-color); 71 | text-decoration: none; 72 | padding: 0.25em 0.5em; 73 | border-radius: 0.25em; 74 | 75 | &:hover { 76 | border-color: currentColor; 77 | } 78 | } 79 | 80 | .btn--reset { 81 | background: transparent; 82 | border: none; 83 | font-size: 1em; 84 | } 85 | 86 | .btnGroup { 87 | display: flex; 88 | border: 1px solid var(--light-border-color); 89 | 90 | &--btn { 91 | display: inline-block; 92 | font-size: 1em; 93 | padding: 0.5em 1em; 94 | background: #fff; 95 | white-space: nowrap; 96 | 97 | text-decoration: none; 98 | 99 | &.isActive { 100 | text-decoration: underline; 101 | text-underline-offset: 0.2em; 102 | text-decoration-thickness: 2px; 103 | color: var(--primary-color); 104 | } 105 | 106 | &:hover, 107 | &:active { 108 | } 109 | 110 | & + & { 111 | border-left: 1px solid var(--light-border-color); 112 | } 113 | } 114 | } 115 | 116 | /* Safari need a default transform 'position' 117 | for elements in svg -> .rotate is a class of svg path */ 118 | [data-menu-button] .rotate { 119 | transform: rotate(0deg); 120 | } 121 | 122 | [data-menu-button].is-active { 123 | .rotate { 124 | transform: scale(1, 0.75) translate(1.5em, -1.375em) rotate(25deg); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /site/_includes/css/components/_carbon.css: -------------------------------------------------------------------------------- 1 | #carbonads { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, 4 | sans-serif; 5 | 6 | margin: 0 -1em 1em; 7 | } 8 | 9 | #carbonads { 10 | display: flex; 11 | max-width: 330px; 12 | background-color: hsl(0, 0%, 98%); 13 | box-shadow: 0 1px 6px 1px hsla(0, 0%, 0%, 0.1); 14 | } 15 | 16 | #carbonads a { 17 | color: inherit; 18 | text-decoration: none; 19 | } 20 | 21 | #carbonads a:hover { 22 | color: inherit; 23 | } 24 | 25 | #carbonads span { 26 | position: relative; 27 | display: block; 28 | overflow: hidden; 29 | } 30 | 31 | #carbonads .carbon-wrap { 32 | display: flex; 33 | } 34 | 35 | .carbon-img { 36 | display: block; 37 | margin: 0; 38 | line-height: 1; 39 | } 40 | 41 | .carbon-img img { 42 | display: block; 43 | max-width: 5em; 44 | } 45 | 46 | .carbon-text { 47 | font-size: 12px; 48 | padding: 10px; 49 | line-height: 1.25; 50 | text-align: left; 51 | } 52 | 53 | .carbon-poweredby { 54 | display: block; 55 | padding: 8px 10px; 56 | background: repeating-linear-gradient( 57 | -45deg, 58 | transparent, 59 | transparent 5px, 60 | hsla(0, 0%, 0%, 0.025) 5px, 61 | hsla(0, 0%, 0%, 0.025) 10px 62 | ) 63 | hsla(203, 11%, 95%, 0.4); 64 | text-align: center; 65 | text-transform: uppercase; 66 | letter-spacing: 0.5px; 67 | font-weight: 600; 68 | font-size: 9px; 69 | line-height: 1; 70 | } 71 | -------------------------------------------------------------------------------- /site/_includes/css/components/_card.css: -------------------------------------------------------------------------------- 1 | .card-link { 2 | &::before { 3 | content: ''; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | 9 | padding-bottom: 60%; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /site/_includes/css/components/_nav.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | li { 3 | position: relative; 4 | } 5 | 6 | a { 7 | display: block; 8 | padding: 0.25em 1em; 9 | text-decoration: none; 10 | 11 | &:hover { 12 | color: var(--primary-color); 13 | text-decoration: underline; 14 | text-underline-offset: 0.2em; 15 | text-decoration-thickness: 0.1em; 16 | } 17 | 18 | &[aria-current='page'] { 19 | background: #fff; 20 | border-radius: 0.5em; 21 | color: var(--primary-color); 22 | } 23 | } 24 | 25 | .nav-tagCount { 26 | position: absolute; 27 | right: 1.25em; 28 | top: 0.375em; 29 | padding: 0.25em 0.5em; 30 | font-size: 0.75em; 31 | font-weight: 700; 32 | color: #686c7a; 33 | border-radius: 0.375em; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /site/_includes/css/components/_skip-link.css: -------------------------------------------------------------------------------- 1 | .skip-link { 2 | position: absolute; 3 | top: -999px; 4 | left: -999px; 5 | height: 1px; 6 | width: 1px; 7 | overflow: hidden; 8 | background: var(--canvas-bg-color); 9 | 10 | &:active, 11 | &:focus, 12 | &:hover { 13 | left: 1em; 14 | top: 1em; 15 | width: auto; 16 | height: auto; 17 | overflow: visible; 18 | } 19 | } -------------------------------------------------------------------------------- /site/_includes/css/index.css: -------------------------------------------------------------------------------- 1 | @import './_base.css'; 2 | @import './components/_buttons.css'; 3 | @import './components/_carbon.css'; 4 | @import './components/_card.css'; 5 | @import './components/_nav.css'; 6 | @import './components/_skip-link.css'; 7 | @import './util/_colors.css'; 8 | @import './util/_dimensions.css'; 9 | @import './util/_fonts.css'; 10 | @import './util/_flex.css'; 11 | @import './util/_grid.css'; 12 | @import './util/_paddings.css'; 13 | @import './util/_position.css'; 14 | @import './util/_margins.css'; 15 | @import './util/_shadows.css'; 16 | @import './util/_visibility.css'; 17 | @import './_fonts.css'; 18 | 19 | body { 20 | --primary-color: #308d34; 21 | --text-color: #106d14; 22 | --text-highlight-color: #005d04; 23 | --light-border-color: #76da8f; 24 | 25 | --canvas-bg-color: #f4fcf4; 26 | --canvas-light-color: #fff; 27 | --sidebar-bg-color: #ddfae0; 28 | 29 | --desktop-sidebar-width: 20em; 30 | --desktop-topbar-height: 5em; 31 | 32 | background: var(--canvas-bg-color); 33 | } 34 | 35 | .container { 36 | @media (min-width: 55em) { 37 | display: grid; 38 | height: 100%; 39 | grid-template-columns: var(--desktop-sidebar-width) 1fr; 40 | grid-template-rows: var(--desktop-topbar-height) 1fr; 41 | } 42 | } 43 | 44 | .main-grid { 45 | display: grid; 46 | grid-template-columns: repeat(auto-fill, minmax(17.5em, 1fr)); 47 | grid-gap: 3em 2em; 48 | } 49 | 50 | .social-grid { 51 | padding: 1em 3em; 52 | grid-gap: 1em; 53 | display: grid; 54 | grid-template-columns: repeat(auto-fill, minmax(10em, 1fr)); 55 | place-items: center; 56 | 57 | justify-content: center; 58 | @media (min-width: 55em) { 59 | grid-template-columns: repeat(3, min-content); 60 | justify-content: end; 61 | } 62 | } 63 | 64 | .flex-column { 65 | display: flex; 66 | flex-direction: column; 67 | } 68 | 69 | .thin-border { 70 | border: 1px solid var(--light-border-color); 71 | } 72 | 73 | .thin-border-bottom { 74 | border-bottom: 1px solid var(--light-border-color); 75 | } 76 | 77 | .hide .thin-border-bottom { 78 | border-bottom: 1px solid var(--light-border-color); 79 | } 80 | 81 | .padding-sidebar { 82 | padding: 1em 2em; 83 | @media (min-width: 55em) { 84 | padding: 1em 3em; 85 | } 86 | } 87 | 88 | .c-logo { 89 | display: flex; 90 | align-items: center; 91 | justify-content: space-between; 92 | 93 | @media (min-width: 55em) { 94 | display: grid; 95 | place-items: center; 96 | justify-items: baseline; 97 | } 98 | } 99 | 100 | .img-size-m { 101 | width: 2.5em; 102 | height: 2.5em; 103 | } 104 | 105 | .img-size-s { 106 | width: 2em; 107 | height: 2em; 108 | } 109 | 110 | .img-size-xs { 111 | width: 1em; 112 | height: 1em; 113 | } 114 | 115 | .img-size-l { 116 | width: 3em; 117 | height: 3em; 118 | } 119 | 120 | .circle { 121 | border-radius: 50%; 122 | overflow: hidden; 123 | border: 2px solid currentColor; 124 | } 125 | 126 | .inline-list { 127 | > li { 128 | display: inline-block; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /site/_includes/css/util/_colors.css: -------------------------------------------------------------------------------- 1 | .bg-lighter { 2 | background: var(--canvas-light-color); 3 | } 4 | 5 | .bg-light { 6 | background: var(--canvas-bg-color); 7 | } 8 | 9 | .bg-medium { 10 | background: var(--sidebar-bg-color); 11 | } 12 | 13 | .color-primary { 14 | color: var(--primary-color); 15 | } 16 | -------------------------------------------------------------------------------- /site/_includes/css/util/_dimensions.css: -------------------------------------------------------------------------------- 1 | .height-100 { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /site/_includes/css/util/_flex.css: -------------------------------------------------------------------------------- 1 | .flex-x-sb { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | 7 | .flex-x-start { 8 | display: flex; 9 | justify-content: start; 10 | align-items: center; 11 | } 12 | -------------------------------------------------------------------------------- /site/_includes/css/util/_fonts.css: -------------------------------------------------------------------------------- 1 | .fst-italic { 2 | font-style: italic; 3 | } 4 | 5 | .fsi-0-875 { 6 | font-size: 0.875em; 7 | } 8 | 9 | .fsi-1 { 10 | font-size: 1em; 11 | } 12 | 13 | .fsi-1-25 { 14 | font-size: 1.25em; 15 | } 16 | 17 | .fsi-1-5 { 18 | font-size: 1.5em; 19 | } 20 | 21 | .fw-bold { 22 | font-weight: bold; 23 | } 24 | 25 | .ff-fancy { 26 | font-family: 'Exo', sans-serif; 27 | } 28 | 29 | .no-underline { 30 | text-decoration: none; 31 | } 32 | -------------------------------------------------------------------------------- /site/_includes/css/util/_grid.css: -------------------------------------------------------------------------------- 1 | .grid-center { 2 | display: grid; 3 | place-items: center; 4 | } 5 | -------------------------------------------------------------------------------- /site/_includes/css/util/_margins.css: -------------------------------------------------------------------------------- 1 | .margin-bottom { 2 | &-xs { 3 | margin-bottom: 0.25rem !important; 4 | } 5 | 6 | &-s { 7 | margin-bottom: 0.5rem !important; 8 | } 9 | 10 | &-m { 11 | margin-bottom: 1rem !important; 12 | } 13 | 14 | &-l { 15 | margin-bottom: 2rem !important; 16 | } 17 | } 18 | 19 | .margin-top { 20 | &-xs { 21 | margin-top: 0.25rem !important; 22 | } 23 | 24 | &-s { 25 | margin-top: 0.5rem !important; 26 | } 27 | 28 | &-m { 29 | margin-top: 1rem !important; 30 | } 31 | 32 | &-l { 33 | margin-top: 2rem !important; 34 | } 35 | } 36 | 37 | .margin-right { 38 | &-xs { 39 | margin-right: 0.25rem !important; 40 | } 41 | 42 | &-s { 43 | margin-right: 0.5rem !important; 44 | } 45 | 46 | &-m { 47 | margin-right: 1rem !important; 48 | } 49 | 50 | &-l { 51 | margin-right: 2rem !important; 52 | } 53 | } 54 | 55 | .neg-margin-left-s { 56 | margin-left: -0.5rem; 57 | } 58 | 59 | .neg-margin-left-m { 60 | margin-left: -1rem; 61 | } 62 | 63 | .neg-margin-right-s { 64 | margin-right: -0.5rem; 65 | } 66 | 67 | .neg-margin-right-m { 68 | margin-right: -1rem; 69 | } 70 | 71 | .margin-top-auto { 72 | margin-top: auto; 73 | } 74 | -------------------------------------------------------------------------------- /site/_includes/css/util/_paddings.css: -------------------------------------------------------------------------------- 1 | .padding { 2 | &-xs { 3 | padding: 0.25rem !important; 4 | } 5 | 6 | &-s { 7 | padding: 0.5rem !important; 8 | } 9 | 10 | &-m { 11 | padding: 1rem !important; 12 | } 13 | 14 | &-l { 15 | padding: 2rem !important; 16 | } 17 | 18 | &-v-m { 19 | padding: 0 1em; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /site/_includes/css/util/_position.css: -------------------------------------------------------------------------------- 1 | .p-relative { 2 | position: relative; 3 | } 4 | 5 | .p-abs-top-right { 6 | position: absolute; 7 | top: 1em; 8 | right: 1em; 9 | } 10 | 11 | .p-abs-top-left { 12 | position: absolute; 13 | top: 1em; 14 | left: 1em; 15 | } 16 | 17 | .z-1 { 18 | z-index: 1; 19 | } 20 | -------------------------------------------------------------------------------- /site/_includes/css/util/_shadows.css: -------------------------------------------------------------------------------- 1 | .shadow-left { 2 | box-shadow: inset -0.5em 0 1.25em -0.875em #d4dadf; 3 | } 4 | 5 | .shadow-full { 6 | box-shadow: 0 0.25em 1em #eee; 7 | } 8 | -------------------------------------------------------------------------------- /site/_includes/css/util/_visibility.css: -------------------------------------------------------------------------------- 1 | .hide-large { 2 | @media (min-width: 55em) { 3 | display: none; 4 | } 5 | } 6 | 7 | .hide-small { 8 | display: none; 9 | 10 | @media (min-width: 55em) { 11 | display: block; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /site/_includes/js/main.js: -------------------------------------------------------------------------------- 1 | const loadCarbonJS = function (src, ref) { 2 | const script = window.document.createElement('script'); 3 | script.src = src; 4 | script.id = '_carbonads_js'; 5 | script.async = true; 6 | ref.append(script); 7 | }; 8 | 9 | const mql = window.matchMedia('(max-width: 55em)'); 10 | const toggleMenuButton = document.querySelector('[data-menu-button]'); 11 | const menu = document.querySelector('[data-menu]'); 12 | let menuInitialized = false; 13 | 14 | mql.addListener((event) => { 15 | if (!menuInitialized && event.matches) { 16 | initMenuToggle(); 17 | } 18 | 19 | if (event.matches) { 20 | menu.hidden = true; 21 | } else { 22 | menu.hidden = false; 23 | } 24 | }); 25 | 26 | if (mql.matches) { 27 | initMenuToggle(); 28 | menu.hidden = true; 29 | } 30 | 31 | const navIsVisible = !mql.matches; 32 | const carbonContainer = document.getElementById('carbon-container'); 33 | 34 | if (navIsVisible && !!carbonContainer) { 35 | // todo 36 | // loadCarbonJS( 37 | // '//cdn.carbonads.com/carbon.js?serve=CE7I4KJL&placement=tiny-helpersdev', 38 | // carbonContainer 39 | // ); 40 | } 41 | 42 | function initMenuToggle() { 43 | toggleMenuButton.setAttribute('aria-expanded', false); 44 | toggleMenuButton.setAttribute('aria-controls', 'menu'); 45 | 46 | toggleMenuButton.addEventListener('click', function () { 47 | if (mql.matches) { 48 | let expanded = this.getAttribute('aria-expanded') === 'true' || false; 49 | this.setAttribute('aria-expanded', !expanded); 50 | toggleMenuButton.classList.toggle('is-active'); 51 | menu.hidden = !menu.hidden; 52 | } 53 | }); 54 | 55 | menuInitialized = true; 56 | } 57 | -------------------------------------------------------------------------------- /site/_includes/templates/_list.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if tag.name == "All" %} 4 |

All tools

5 | {% else %} 6 |

"{{ tag.name }}" teachers

7 | {% endif %} 8 | 9 |
10 | Sort by name 13 | Sort by date 16 |
17 |
18 |
    19 | {% for item in tag.items | sort(renderData.listIsReversed, false, renderData.listIsSortedBy )%} 20 |
  1. 21 | Screenshot of {{item.name}} 28 | 29 |
    30 |

    {{ item.name }}

    31 | 43 |

    {{ item.desc }}

    44 |
    45 |
    Added {{ item.addedAt | prettyDate }}
    46 | {% if item.isPaid %} 47 |
    48 | Costs money 49 | 50 |
    51 | {% endif %} 52 | {% if item.maintainers.length %} 53 |
    54 |

    Made by

    55 |
      56 | {% for maintainer in item.maintainers %} 57 |
    • 58 | 59 | GitHub profile image of {{ maintainer }} 64 | 65 |
    • 66 | {% endfor %} 67 |
    68 |
    69 | {% endif %} 70 | Go to {{ item.name }} 71 |
  2. 72 | {% endfor %} 73 |
74 |
75 | -------------------------------------------------------------------------------- /site/_includes/templates/_logo.njk: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /site/_includes/templates/_navigation.njk: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /site/_includes/templates/_social.njk: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | This project uses Vercel routes the root to /home when deployed. Navigate to home. 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /site/tag-sorted-by-date.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | pagination: 4 | data: tags 5 | size: 1 6 | alias: tag 7 | permalink: "/{{ tag.slug }}/latest/" 8 | renderData: 9 | isCanonical: true 10 | title: "{{ tag.name }}" 11 | listIsSortedBy: "addedAt" 12 | listIsReversed: true 13 | --- -------------------------------------------------------------------------------- /site/tag.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | pagination: 4 | data: tags 5 | size: 1 6 | alias: tag 7 | permalink: "/{{ tag.slug }}/" 8 | renderData: 9 | title: "{{ tag.name }}" 10 | listIsSortedBy: "name" 11 | listIsReversed: false 12 | --- -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/exo.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/exo.woff -------------------------------------------------------------------------------- /static/exo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/exo.woff2 -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/favicon.ico -------------------------------------------------------------------------------- /static/large-media-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanjudis/tiny-teachers/986b31da9e26ae261996886649912803bd2ec781/static/large-media-image.jpg -------------------------------------------------------------------------------- /static/paid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard 8 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /teachers/a11yphant.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a11yphant", 3 | "desc": "Learn more about web accessibility.", 4 | "url": "https://a11yphant.com/", 5 | "tags": [ 6 | "Accessibility" 7 | ], 8 | "maintainers": ["a11yphant"], 9 | "loginRequired": false, 10 | "isPaid": false, 11 | "addedAt": "2022-03-20" 12 | } 13 | -------------------------------------------------------------------------------- /teachers/can't-unsee.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Can't unsee", 3 | "desc": "Get better at spotting design differences.", 4 | "url": "https://cantunsee.space/", 5 | "tags": ["Design"], 6 | "maintainers": [], 7 | "addedAt": "2021-10-03" 8 | } 9 | -------------------------------------------------------------------------------- /teachers/css-animation-course.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CSS Animation course", 3 | "desc": "Learn how to use CSS Animations.", 4 | "url": "https://css-animations.io/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/css-diner.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CSS Diner", 3 | "desc": "Learn how to use CSS selectors.", 4 | "url": "https://flukeout.github.io/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [ 9 | "flukeout" 10 | ], 11 | "addedAt": "2021-10-03" 12 | } 13 | -------------------------------------------------------------------------------- /teachers/css-for-js-developers.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CSS for JS developers", 3 | "desc": "Learn all about CSS.", 4 | "url": "https://css-for-js.dev/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "loginRequired": true, 10 | "isPaid": true, 11 | "addedAt": "2021-10-03" 12 | } 13 | -------------------------------------------------------------------------------- /teachers/design-patterns-game.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Design Patterns Game", 3 | "desc": "Learn common JavaScript design patterns.", 4 | "url": "https://designpatternsgame.com/", 5 | "tags": ["JavaScript"], 6 | "maintainers": [ 7 | "zoltantothcom" 8 | ], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/designercize.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Designercize", 3 | "desc": "Train your design skills with random assignments.", 4 | "url": "https://designercize.com/", 5 | "tags": [ 6 | "Design" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/divize.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Divize", 3 | "desc": "Learn, Write and Master HTML/CSS Through Real UI Challenges", 4 | "url": "https://divize.io", 5 | "tags": [ 6 | "Accessibility", 7 | "CSS", 8 | "HTML" 9 | ], 10 | "maintainers": [ 11 | "arbaouimehdi", 12 | "mad42" 13 | ], 14 | "loginRequired": true, 15 | "isPaid": true, 16 | "addedAt": "2023-07-05" 17 | } -------------------------------------------------------------------------------- /teachers/flexbox-adventures.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flexbox adventures", 3 | "desc": "Learn how to use CSS flexbox.", 4 | "url": "https://codingfantasy.com/games/flexboxadventure/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/flexbox-defense.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flexbox defense", 3 | "desc": "Learn how to use CSS flexbox.", 4 | "url": "http://www.flexboxdefense.com/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/flexbox-froggy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flexbox Froggy", 3 | "desc": ":Learn how to use CSS flexbox.", 4 | "url": "https://flexboxfroggy.com/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [ 9 | "thomaspark" 10 | ], 11 | "addedAt": "2021-10-03" 12 | } 13 | -------------------------------------------------------------------------------- /teachers/future-coder.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Future Coder", 3 | "desc": "Learn Python from scratch.", 4 | "url": "https://futurecoder.io/", 5 | "tags": [ 6 | "Python" 7 | ], 8 | "maintainers": [ 9 | "alexmojaki" 10 | ], 11 | "loginRequired": false, 12 | "isPaid": false, 13 | "addedAt": "2021-10-08" 14 | } 15 | -------------------------------------------------------------------------------- /teachers/grid-garden.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grid garden", 3 | "desc": "Learn how to use CSS grid.", 4 | "url": "https://cssgridgarden.com/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/jsrobot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JSRobot", 3 | "desc": "Learn JavaScript by progamming a robot.", 4 | "url": "https://lab.reaal.me/jsrobot/", 5 | "tags": [ 6 | "JavaScript" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/knights-of-the-flexbox-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Knights of the Flexbox Table", 3 | "desc": "Learn how to use CSS flexbox.", 4 | "url": "https://knightsoftheflexboxtable.com/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/learn-git-branching.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Learn git branching", 3 | "desc": "Learn how to use git.", 4 | "url": "https://learngitbranching.js.org/", 5 | "tags": [ 6 | "git" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/learn-html-css.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Learn HTML CSS", 3 | "desc": "Learn semantic and accessible HTML and CSS step by step.", 4 | "url": "https://learnhtmlcss.online/", 5 | "tags": [ 6 | "CSS", 7 | "HTML" 8 | ], 9 | "maintainers": [], 10 | "loginRequired": false, 11 | "isPaid": false, 12 | "addedAt": "2022-05-13" 13 | } 14 | -------------------------------------------------------------------------------- /teachers/mess-with-dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mess with DNS", 3 | "desc": "Learn DNS by setting up dummy DNS records.", 4 | "url": "https://messwithdns.net/", 5 | "tags": [ 6 | "System Administration" 7 | ], 8 | "maintainers": [], 9 | "loginRequired": false, 10 | "isPaid": false, 11 | "addedAt": "2021-12-21" 12 | } 13 | -------------------------------------------------------------------------------- /teachers/open-vim.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Open Vim", 3 | "desc": "Learn how to use Vim.", 4 | "url": "https://www.openvim.com/", 5 | "tags": [ 6 | "Vim" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/react-hooks-cheatsheet.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Hooks Cheatsheet", 3 | "desc": "Learn about essential React Hooks", 4 | "url": "https://react-hooks-cheatsheet.com/", 5 | "tags": [ 6 | "Cheatsheet", 7 | "JavaScript" 8 | ], 9 | "maintainers": [ 10 | "ohansemmanuel" 11 | ], 12 | "loginRequired": false, 13 | "isPaid": false, 14 | "addedAt": "2021-10-31" 15 | } 16 | -------------------------------------------------------------------------------- /teachers/react-tutorial.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Tutorial", 3 | "desc": "Learn how to use React.", 4 | "url": "https://react-tutorial.app/app.html", 5 | "tags": [ 6 | "JavaScript" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/regex-crossword.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Regex Crossword", 3 | "desc": "Learn how to use regular expressions.", 4 | "url": "https://regexcrossword.com/", 5 | "tags": [ 6 | "Regular expressions" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/regexlearn.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RegexLearn", 3 | "desc": "Learn RegEx step by step, from zero to advanced.", 4 | "url": "https://regexlearn.com/", 5 | "tags": [ 6 | "Regular expressions" 7 | ], 8 | "maintainers": [ 9 | "aykutkardas" 10 | ], 11 | "loginRequired": false, 12 | "isPaid": false, 13 | "addedAt": "2021-11-18" 14 | } 15 | -------------------------------------------------------------------------------- /teachers/regexone.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RegexOne", 3 | "desc": "Learn how to use regular expressions.", 4 | "url": "https://regexone.com/", 5 | "tags": [ 6 | "Regular expressions" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/select-star-sql.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Select Star SQL", 3 | "desc": "Learn SQL.", 4 | "url": "https://selectstarsql.com/", 5 | "tags": [ 6 | "SQL" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/sqlbolt.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SQLBolt", 3 | "desc": "Learn how to write SQL.", 4 | "url": "https://sqlbolt.com/", 5 | "tags": [ 6 | "SQL" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/the-bezier-game.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The Bézier game", 3 | "desc": "Level up your pen tool skills.", 4 | "url": "https://bezier.method.ac/", 5 | "tags": [ 6 | "Design" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/the-boolean-game.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The Boolean game", 3 | "desc": "Learn how to create shapes in design tools.", 4 | "url": "https://boolean.method.ac/", 5 | "tags": [ 6 | "Design" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/the-font-game.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The Font Game", 3 | "desc": "Learn to recognize fonts.", 4 | "url": "https://ilovetypography.com/2020/05/31/the-font-game", 5 | "tags": ["Fonts"], 6 | "maintainers": [], 7 | "addedAt": "2021-10-03" 8 | } 9 | -------------------------------------------------------------------------------- /teachers/typescript-exercises.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TypeScript exercises", 3 | "desc": "Learn how to use TypeScript.", 4 | "url": "https://typescript-exercises.github.io/", 5 | "tags": ["TypeScript"], 6 | "maintainers": [], 7 | "addedAt": "2021-10-03" 8 | } 9 | -------------------------------------------------------------------------------- /teachers/typewar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typewar", 3 | "desc": "Learn to recognize fonts.", 4 | "url": "https://typewar.com/", 5 | "tags": [ 6 | "Fonts" 7 | ], 8 | "maintainers": [], 9 | "addedAt": "2021-10-03" 10 | } 11 | -------------------------------------------------------------------------------- /teachers/web.dev-"learn-css".json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web.dev \"Learn CSS\"", 3 | "desc": "Learn CSS!", 4 | "url": "https://web.dev/learn/css/", 5 | "tags": [ 6 | "CSS" 7 | ], 8 | "maintainers": [], 9 | "loginRequired": false, 10 | "isPaid": false, 11 | "addedAt": "2021-10-30" 12 | } 13 | --------------------------------------------------------------------------------