├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Makefile ├── README.md ├── components ├── content-container.js ├── global.scss ├── header.js ├── image-full.js ├── layout.js ├── left-side.js ├── legal.js ├── link.js ├── made-by.js ├── nav.js ├── pop-up.js ├── refresh-pop-up.js ├── right-side.js ├── scastie.js ├── skb-list.js ├── skb-page.js └── top-container.js ├── jsconfig.json ├── lib ├── clean-object.js ├── discord-link.js ├── environment.js ├── fetch-local-lessons.js ├── hover.js ├── lesson-save.js ├── local-storage.js └── routes.js ├── netlify.toml ├── next-sitemap.config.js ├── next.config.js ├── package.json ├── pages ├── 404.js ├── _app.js ├── _document.js ├── about.js ├── index.js └── scala │ ├── _-placeholder.js │ ├── _-wildcard.js │ ├── abstract-class.js │ ├── applicative.js │ ├── apply.js │ ├── call-by-name-parameters.js │ ├── case-class-copy.js │ ├── case-class-unapply.js │ ├── case-class.js │ ├── case-object.js │ ├── challenge-1.js │ ├── class-new.js │ ├── companion-objects.js │ ├── comparators.js │ ├── constraint-inheritance.js │ ├── contexts.js │ ├── covariance.js │ ├── curry.js │ ├── defined-type.js │ ├── either.js │ ├── enumeration.js │ ├── extractor-pattern.js │ ├── flatmap.js │ ├── foldable.js │ ├── foldleft.js │ ├── for-comprehension.js │ ├── functor.js │ ├── future.js │ ├── generic-trait.js │ ├── higher-kind.js │ ├── implicit-class.js │ ├── implicit-conversion.js │ ├── implicit-proof.js │ ├── implicit-val.js │ ├── infix-notation.js │ ├── list-filter-method.js │ ├── list-flatten.js │ ├── list-of-option-flatten.js │ ├── list-parallel.js │ ├── list-pattern-matching.js │ ├── list-sum-method.js │ ├── list-zip.js │ ├── literal-identifiers.js │ ├── main.js │ ├── map-for-list.js │ ├── method-with-arguments.js │ ├── methods.js │ ├── monad.js │ ├── multiple-inheritance.js │ ├── objects.js │ ├── operators.js │ ├── option-map.js │ ├── option-pattern-matching.js │ ├── option.js │ ├── pattern-matching-at.js │ ├── pattern-matching-for-case-class.js │ ├── pattern-matching-or.js │ ├── pattern-matching.js │ ├── random.js │ ├── range.js │ ├── recursion.js │ ├── regex.js │ ├── repeated-parameters.js │ ├── sealed.js │ ├── self-referred-type.js │ ├── set.js │ ├── star-parameter.js │ ├── stream.js │ ├── string-format.js │ ├── string-interpolation.js │ ├── thread-sleep.js │ ├── trait.js │ ├── traversable.js │ ├── try.js │ ├── tuple.js │ ├── typeclass.js │ ├── unapply-magic.js │ ├── upper-constraint.js │ ├── val-lazy-def.js │ ├── val-pattern-matching.js │ ├── values.js │ └── visibility.js └── public ├── _redirects ├── assets ├── Discord-Logo+Wordmark-Black.svg ├── GitHub-Mark-64px.png ├── Patreon-Logo.png ├── Pinterest-logo.png ├── linkedin-xxl.png ├── medium-logo.png └── tour-of-scala-logo.png ├── favicon ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png └── ms-icon-70x70.png └── scripts └── google.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: leobenkel 4 | patreon: leobenkel 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /.netlify 4 | /netlify 5 | sitemap.xml 6 | package-lock.json 7 | yarn.lock 8 | 9 | # dependencies 10 | /node_modules 11 | /.pnp 12 | .pnp.js 13 | 14 | # testing 15 | /coverage 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env.local 34 | .env.development.local 35 | .env.test.local 36 | .env.production.local 37 | 38 | # Service Worker 39 | public/sw.js 40 | public/sw.js.map 41 | public/workbox*.js 42 | public/workbox*.js.map 43 | 44 | .sass-cache 45 | 46 | *.log -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Launch Program", 8 | "skipFiles": [ 9 | "/**" 10 | ], 11 | "port": 9229 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Workspace settings 2 | { 3 | // The following will hide the js and map files in the editor 4 | "files.exclude": { 5 | ".netlify": true, 6 | "node_modules": true, 7 | ".next": true, 8 | "sitemap.xml": true, 9 | "package-lock.json": true, 10 | "next-env.d.ts": true, 11 | "out": true, 12 | "netlify": true 13 | }, 14 | "cSpell.words": [ 15 | "scastie" 16 | ] 17 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | netlify dev 3 | 4 | clean: 5 | rm -rf node_modules package-lock.json .netlify .next out next-env.d.ts 6 | 7 | install: 8 | npm install 9 | npm audit fix --force || echo '' 10 | 11 | start_dev: 12 | npx next dev -p 4000 13 | 14 | start_prod: 15 | npx next build --debug 16 | NODE_OPTIONS='--inspect' npx next start -p 4000 17 | 18 | start_prod_netlify: 19 | netlify build 20 | cd ./.next 21 | http-server -p 4000 22 | 23 | update_netlify: 24 | npm uninstall -g netlify-cli 25 | npm i -g netlify-cli 26 | npm audit fix 27 | 28 | check_versions: 29 | npm outdated -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Tour of Scala Logo](https://raw.githubusercontent.com/leobenkel/tour-of-scala/main/public/assets/tour-of-scala-logo.png) Tour of Scala 2 | 3 | 4 | [![Netlify Status](https://api.netlify.com/api/v1/badges/23c38894-313e-4663-9f40-5375f008f18e/deploy-status)](https://app.netlify.com/sites/tour-of-scala/deploys) 5 | 6 | Redesign of the articles posted originally at: https://leobenkel.com 7 | 8 | Can now be used at https://tourofscala.com 9 | 10 | Join the discord Scala Learning community at http://sca.la/learning-community 11 | 12 | ## Public Presence 13 | 14 | * [Announcement on LinkedIn](https://www.linkedin.com/posts/leobenkel_scala-opensource-scala-activity-7021055024113836032--1oo) 15 | * [Announcement on Reddit](https://www.reddit.com/r/scala/comments/10e8qdc/big_news_for_the_scala_community_finally_fully) 16 | -------------------------------------------------------------------------------- /components/content-container.js: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { createUseStyles } from 'react-jss' 3 | 4 | 5 | const useStyles = createUseStyles( 6 | { 7 | skbInstructions: { 8 | overflow: 'auto', 9 | padding: '12px', 10 | paddingTop: '0', 11 | fontSize: '1em', 12 | }, 13 | fullscreen: { 14 | paddingLeft: '10%', 15 | paddingRight: '10%' 16 | } 17 | }, 18 | { 19 | name: "ContentContainer" 20 | } 21 | ) 22 | 23 | export default function ContentContainer({ children, fullscreen }) { 24 | const styles = useStyles() 25 | 26 | return
{children}
27 | } -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { createUseStyles } from 'react-jss' 3 | 4 | import useDiscordLink from 'lib/discord-link' 5 | 6 | import ImageFull from 'components/image-full' 7 | import L from 'components/link' 8 | 9 | 10 | const useStyles = createUseStyles( 11 | { 12 | title: { 13 | display: 'flex', 14 | justifyContent: 'space-between', 15 | padding: '10px', 16 | paddingBottom: '0' 17 | }, 18 | articleTitle: { 19 | display: 'flex', 20 | margin: 0, 21 | paddingRight: '8px', 22 | }, 23 | tourOfScalaLogo: { 24 | height: 'fit-content' 25 | }, 26 | titleLinks: { 27 | '& a': { 28 | textDecoration: 'none', 29 | color: 'black' 30 | } 31 | }, 32 | discordLink: { 33 | display: 'inline-block', 34 | height: '2em', 35 | '& svg': { 36 | height: '2em', 37 | display: 'block' 38 | } 39 | }, 40 | sourceLink: { 41 | display: 'inline-block', 42 | }, 43 | sourceLinkIcon: { 44 | fontSize: '2em', 45 | transform: 'rotate(-45deg)' 46 | }, 47 | iconImgIcon: { 48 | height: '1.7em' 49 | }, 50 | }, 51 | { 52 | name: "Header" 53 | } 54 | ) 55 | 56 | export default function Header({ title, sourceLink, githubLink }) { 57 | const styles = useStyles() 58 | const discordLink = useDiscordLink() 59 | 60 | return
61 |
62 |

63 | {title ? title : (<>Tour of Scala)} 64 |

65 |
66 | 67 | 68 | 69 | {sourceLink ? 70 | 71 | link 72 | : null} 73 | {githubLink ? 74 | 75 | 76 | : null} 77 |
78 |
79 |
80 | } -------------------------------------------------------------------------------- /components/image-full.js: -------------------------------------------------------------------------------- 1 | import { ReactSVG } from 'react-svg' 2 | 3 | 4 | export default function ImageFull({ alt, src, className, priority, ...props }) { 5 | if (src.endsWith('svg')) { 6 | return ( 7 | 12 | ) 13 | } else { 14 | if (!alt) console.error(`No ALT for image: '${src}'`) 15 | return ( 16 | {`Tour 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /components/left-side.js: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from 'react-jss' 2 | 3 | 4 | const useStyles = createUseStyles( 5 | { 6 | left: { 7 | display: 'flex', 8 | height: '100%', 9 | minHeight: '100%', 10 | maxHeight: '100%', 11 | backgroundColor: '#ececec', 12 | padding: '0', 13 | boxSizing: 'border-box', 14 | flexDirection: 'column', 15 | flexWrap: 'nowrap', 16 | justifyContent: 'space-between', 17 | borderRight: '2px #7c7c7c2f solid', 18 | width: 'auto', 19 | flex: '1 1 45vw', 20 | overflow: 'auto' 21 | } 22 | }, 23 | { 24 | name: "LeftSide" 25 | } 26 | ) 27 | 28 | export default function LeftSide({ children }) { 29 | const styles = useStyles() 30 | 31 | return
32 | {children} 33 |
34 | } -------------------------------------------------------------------------------- /components/legal.js: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, 3 | useState, 4 | } from 'react' 5 | 6 | import dynamic from 'next/dynamic' 7 | 8 | import { isDev } from 'lib/environment' 9 | import { 10 | readStorage, 11 | writeStorage, 12 | } from 'lib/local-storage' 13 | 14 | 15 | const Head = dynamic(() => import('next/head')) 16 | const PopUp = dynamic(() => import('components/pop-up')) 17 | 18 | export default function Legal() { 19 | const [isUndefined, setIsUndefined] = useState(true) 20 | const [shouldShowLegal, setShouldShowLegal] = useState(false) 21 | 22 | // date to be able to void in case of legal update 23 | const storageKey = 'legal-banner-accepted-202106-15' 24 | 25 | useEffect(() => { 26 | const storageRead = readStorage(storageKey, false) 27 | updateStatus(storageRead) 28 | }, []) 29 | 30 | const updateStatus = (isRead) => { 31 | writeStorage(storageKey, isRead, 24 * 365) 32 | setShouldShowLegal(!isRead) 33 | setIsUndefined(false) 34 | } 35 | 36 | const acknowledge = () => updateStatus(true) 37 | 38 | if (shouldShowLegal) { 39 | return 43 |

44 | By continuing to be using this website, you are allowing the use of anonymized tracking data. 45 |

46 |
47 | } else if (isUndefined) { 48 | return null 49 | } else if (isDev) { 50 | return ( 51 | 52 | 53 | 54 | ) 55 | } else { 56 | return ( 57 | 58 | {/* */} 59 |