├── .nvmrc ├── .eslintignore ├── public ├── favicon.ico ├── og-image.png ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── learn │ └── basics │ │ ├── introduction-css-how-the-browser-renders-the-page │ │ └── style.css │ │ ├── styling-text │ │ └── JunctionVariableGX.ttf │ │ └── selectors-specificity │ │ └── css-selectors-specificity-summary.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest └── sponsors │ └── Theodo.svg ├── .prettierrc ├── components ├── Header │ ├── logo.png │ ├── logo-white.png │ ├── Header.module.scss │ └── index.tsx ├── NextKataButton │ ├── NextKataButton.module.scss │ └── index.tsx ├── KataRating │ ├── blue-heart.png │ ├── KataRating.module.scss │ └── index.tsx ├── Divider │ ├── index.tsx │ └── Divider.module.scss ├── Radio │ ├── Radio.module.scss │ └── index.tsx ├── KataQuestions │ ├── KataQuestions.module.scss │ └── index.tsx ├── Code │ ├── Code.module.scss │ └── index.tsx ├── Input │ ├── Input.module.scss │ └── index.tsx ├── Footer │ ├── Footer.module.scss │ └── index.tsx ├── Image │ ├── Image.module.scss │ └── index.tsx ├── Link │ ├── Link.module.scss │ └── index.tsx ├── Checkbox │ ├── Checkbox.module.scss │ └── index.tsx ├── Table │ ├── Table.module.scss │ └── index.tsx ├── Button │ ├── Button.module.scss │ └── index.tsx ├── Layout │ ├── Layout.module.scss │ └── index.tsx ├── Summary │ ├── Summary.module.scss │ └── index.tsx ├── Exercise │ ├── Exercise.module.scss │ └── index.tsx ├── Editor │ ├── Editor.module.scss │ └── index.tsx └── Skill │ └── index.tsx ├── services ├── css-rule-question.png ├── storage.ts ├── pages.tsx └── skills.tsx ├── .gitignore ├── app ├── (main) │ ├── learn │ │ └── basics │ │ │ ├── the-flow-layout │ │ │ ├── anonymous-box.png │ │ │ ├── layout.tsx │ │ │ └── flow-layout.svg │ │ │ ├── the-box-model-and-layouts │ │ │ ├── devtools.png │ │ │ ├── layout.tsx │ │ │ ├── box-model.svg │ │ │ ├── border-box-model.svg │ │ │ └── page.tsx │ │ │ ├── css-units-variables │ │ │ ├── twitter-profile.png │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ ├── selectors-specificity │ │ │ ├── simple-selectors.png │ │ │ └── layout.tsx │ │ │ ├── styling-text-custom-fonts │ │ │ ├── roboto-font-family.png │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ └── introduction-css-how-the-browser-renders-the-page │ │ │ ├── profiler.png │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── css-rule.svg │ ├── Analytics.tsx │ ├── MainLayout.module.scss │ ├── skills-list │ │ ├── layout.tsx │ │ ├── SkillsList.module.scss │ │ └── page.tsx │ ├── Home.module.scss │ ├── global.scss │ ├── layout.tsx │ └── page.tsx ├── layout.tsx ├── not-found.tsx └── (dojo) │ ├── layout.tsx │ └── dojo │ └── [kataId] │ ├── Dojo.module.scss │ ├── page.tsx │ └── ButtonLinks.tsx ├── .vscode ├── extensions.json └── settings.json ├── .editorconfig ├── next-env.d.ts ├── next.config.js ├── .github └── workflows │ └── tests.yml ├── stylelint.config.js ├── README.md ├── stylesheet.scss ├── tsconfig.json ├── package.json └── .eslintrc.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.17 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/og-image.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true 5 | } 6 | -------------------------------------------------------------------------------- /components/Header/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/components/Header/logo.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /components/Header/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/components/Header/logo-white.png -------------------------------------------------------------------------------- /components/NextKataButton/NextKataButton.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | text-align: center; 3 | margin-top: 2rem; 4 | } 5 | -------------------------------------------------------------------------------- /public/learn/basics/introduction-css-how-the-browser-renders-the-page/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-style: italic; 3 | } 4 | -------------------------------------------------------------------------------- /services/css-rule-question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/services/css-rule-question.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /components/KataRating/blue-heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/components/KataRating/blue-heart.png -------------------------------------------------------------------------------- /components/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Divider.module.scss'; 2 | 3 | export const Divider = () =>
; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | /build 4 | .DS_Store 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | .vercel 9 | *.tsbuildinfo 10 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/the-flow-layout/anonymous-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/app/(main)/learn/basics/the-flow-layout/anonymous-box.png -------------------------------------------------------------------------------- /public/learn/basics/styling-text/JunctionVariableGX.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/learn/basics/styling-text/JunctionVariableGX.ttf -------------------------------------------------------------------------------- /app/(main)/learn/basics/the-box-model-and-layouts/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/app/(main)/learn/basics/the-box-model-and-layouts/devtools.png -------------------------------------------------------------------------------- /app/(main)/Analytics.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { GoogleAnalytics } from 'nextjs-google-analytics'; 4 | 5 | export const Analytics = () => ; 6 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/css-units-variables/twitter-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/app/(main)/learn/basics/css-units-variables/twitter-profile.png -------------------------------------------------------------------------------- /app/(main)/learn/basics/selectors-specificity/simple-selectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/app/(main)/learn/basics/selectors-specificity/simple-selectors.png -------------------------------------------------------------------------------- /components/Radio/Radio.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .input { 4 | cursor: pointer; 5 | width: 1rem; 6 | height: 1rem; 7 | accent-color: style.$blue; 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "Orta.vscode-jest", 6 | "stylelint.vscode-stylelint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/styling-text-custom-fonts/roboto-font-family.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/app/(main)/learn/basics/styling-text-custom-fonts/roboto-font-family.png -------------------------------------------------------------------------------- /components/KataQuestions/KataQuestions.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .question { 4 | display: block; 5 | } 6 | 7 | .link { 8 | @include style.small-font; 9 | } 10 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.NotFound); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /public/learn/basics/selectors-specificity/css-selectors-specificity-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/public/learn/basics/selectors-specificity/css-selectors-specificity-summary.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 100 11 | -------------------------------------------------------------------------------- /app/(main)/MainLayout.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .main-container { 4 | max-width: style.$page-width; 5 | margin: style.$main-vertical-margin auto; 6 | padding: style.$main-vertical-padding; 7 | } 8 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/introduction-css-how-the-browser-renders-the-page/profiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbericTrancart/cssdojo/HEAD/app/(main)/learn/basics/introduction-css-how-the-browser-renders-the-page/profiler.png -------------------------------------------------------------------------------- /app/(main)/skills-list/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.SkillsList); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /components/Code/Code.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .code { 4 | background-color: style.$lightGrey; 5 | padding: 0.1rem 0.2rem; 6 | border-radius: 0.1rem; 7 | 8 | @include style.code-font; 9 | } 10 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/the-flow-layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.FlowLayout); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/css-units-variables/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.CSSUnitsVariables); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/the-box-model-and-layouts/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.TheBoxModel); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/selectors-specificity/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.SelectorsSpecificity); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/styling-text-custom-fonts/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.StylingTextCustomFonts); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /components/Divider/Divider.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .divider { 4 | display: flex; 5 | flex-basis: 100%; 6 | align-items: center; 7 | border: none; 8 | margin: 2rem 0; 9 | border-top: 1px solid style.$lightGrey; 10 | } 11 | -------------------------------------------------------------------------------- /components/Input/Input.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .input { 4 | border-radius: 0.1rem; 5 | accent-color: style.$blue; 6 | border: 1px solid style.$darkGrey; 7 | padding: 0.25rem; 8 | outline-offset: 2px; 9 | font: inherit; 10 | } 11 | -------------------------------------------------------------------------------- /app/(main)/learn/basics/introduction-css-how-the-browser-renders-the-page/layout.tsx: -------------------------------------------------------------------------------- 1 | import { generateTitleMetadata, IndentityLayout, PAGES } from 'services/pages'; 2 | 3 | export const metadata = generateTitleMetadata(PAGES.WhyCSSHowItWorks); 4 | 5 | export default IndentityLayout; 6 | -------------------------------------------------------------------------------- /components/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .container { 4 | max-width: style.$page-width; 5 | margin: 0 auto; 6 | padding: 0 1rem 2rem; 7 | text-decoration: italic; 8 | text-align: center; 9 | 10 | @include style.small-font; 11 | } 12 | -------------------------------------------------------------------------------- /app/(main)/skills-list/SkillsList.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .fieldset { 4 | border: none; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | .legend { 10 | font-weight: bold; 11 | } 12 | 13 | .label { 14 | display: block; 15 | margin-top: 0.5rem; 16 | } 17 | -------------------------------------------------------------------------------- /components/Image/Image.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .figure { 4 | margin: 1.5rem 0; 5 | text-align: center; 6 | } 7 | 8 | .caption { 9 | font-style: italic; 10 | } 11 | 12 | .image { 13 | max-width: 100%; 14 | height: auto; 15 | margin: 0 auto; 16 | } 17 | -------------------------------------------------------------------------------- /components/Link/Link.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .link { 4 | cursor: pointer; 5 | transition: color ease 0.3s; 6 | text-decoration: underline; 7 | color: style.$darkGrey; 8 | 9 | &:hover, 10 | &:focus, 11 | &:active { 12 | color: style.$blue; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'components/Link'; 2 | import Layout from './(main)/layout'; 3 | 4 | const Error404 = () => ( 5 | 6 |

Not Found!

7 | 8 |

This is not the CSS property you’re looking for.

9 | 10 | Return Home 11 |
12 | ); 13 | 14 | export default Error404; 15 | -------------------------------------------------------------------------------- /components/Checkbox/Checkbox.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .input { 4 | display: inline-block; 5 | position: relative; 6 | border: 2px solid style.$darkGrey; 7 | cursor: pointer; 8 | vertical-align: middle; 9 | width: 1rem; 10 | height: 1rem; 11 | border-radius: 0.1rem; 12 | accent-color: style.$blue; 13 | } 14 | -------------------------------------------------------------------------------- /components/Code/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Code.module.scss'; 4 | 5 | export const Code = ({ children, className, ...rest }: ComponentProps<'kbd'>) => ( 6 | 7 | {children} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /components/Table/Table.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | @mixin cell() { 4 | text-align: left; 5 | border: 1px solid style.$lightGrey; 6 | padding: 0.5rem 1rem; 7 | } 8 | 9 | .table { 10 | border-collapse: collapse; 11 | margin: 0 auto; 12 | } 13 | 14 | .table-header-cell { 15 | @include cell; 16 | } 17 | 18 | .table-cell { 19 | @include cell; 20 | } 21 | -------------------------------------------------------------------------------- /components/NextKataButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonLink } from 'components/Button'; 2 | 3 | import styles from './NextKataButton.module.scss'; 4 | 5 | interface Props { 6 | href: string; 7 | } 8 | 9 | export const NextKataButton: React.FC = ({ href }) => ( 10 |
11 | Go to the next kata 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.trimFinalNewlines": true, 4 | "files.trimTrailingWhitespace": true, 5 | "files.insertFinalNewline": true, 6 | 7 | "[javascript]": { 8 | "editor.tabSize": 2 9 | }, 10 | "javascript.validate.enable": false, 11 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 12 | "[json]": { 13 | "editor.tabSize": 2 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/storage.ts: -------------------------------------------------------------------------------- 1 | export const getItem = (key: string) => { 2 | try { 3 | return localStorage.getItem(key); 4 | } catch { 5 | console.warn('There was an issue with local storage!'); 6 | } 7 | 8 | return null; 9 | }; 10 | 11 | export const setItem = (key: string, value: string) => { 12 | try { 13 | localStorage.setItem(key, value); 14 | } catch { 15 | console.warn('There was an issue with local storage!'); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | webpack: (config) => { 4 | config.module.rules.push({ 5 | test: /\.svg$/i, 6 | use: [ 7 | { 8 | loader: '@svgr/webpack', 9 | options: { 10 | svgProps: { role: 'img', width: '100%', height: 'auto' }, 11 | titleProp: true, 12 | }, 13 | }, 14 | ], 15 | }); 16 | return config; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /components/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .button { 4 | display: inline-block; 5 | padding: 0.5rem 1rem; 6 | border: 1px solid style.$darkGrey; 7 | cursor: pointer; 8 | background-color: transparent; 9 | font: inherit; 10 | color: inherit; 11 | text-decoration: none; 12 | text-align: center; 13 | transition: background-color 0.2s ease-in-out; 14 | 15 | &:hover, 16 | &:focus { 17 | background-color: style.$lighterGrey; 18 | } 19 | } 20 | 21 | .small { 22 | padding: 0.25rem 0.5rem; 23 | } 24 | -------------------------------------------------------------------------------- /components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .container { 4 | background-color: style.$darkGrey; 5 | color: style.$white; 6 | padding: 1rem; 7 | } 8 | 9 | .logo { 10 | margin: 0 auto; 11 | height: 6rem; 12 | width: min-content; 13 | } 14 | 15 | .home-link { 16 | display: flex; 17 | flex-direction: column; 18 | text-align: center; 19 | font: inherit; 20 | color: inherit; 21 | text-decoration: none; 22 | } 23 | 24 | .description { 25 | @include style.subtitle-font; 26 | 27 | margin: 0; 28 | } 29 | -------------------------------------------------------------------------------- /app/(dojo)/layout.tsx: -------------------------------------------------------------------------------- 1 | import 'app/(main)/global.scss'; 2 | import { ibmPlexSans } from 'app/(main)/layout'; 3 | import styles from 'app/(main)/MainLayout.module.scss'; 4 | import { generateTitleMetadata, PAGES } from 'services/pages'; 5 | 6 | export const metadata = generateTitleMetadata(PAGES.Dojo); 7 | 8 | const Layout = ({ children }: { children: React.ReactNode }) => ( 9 | 10 | 11 |
{children}
12 | 13 | 14 | ); 15 | 16 | export default Layout; 17 | -------------------------------------------------------------------------------- /components/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .title { 4 | position: relative; 5 | margin: 0; 6 | 7 | @include style.title-font; 8 | } 9 | 10 | .subtitle { 11 | position: relative; 12 | margin: 0; 13 | margin-top: 1rem; 14 | 15 | @include style.subtitle-font; 16 | 17 | &-anchor { 18 | /* stylelint-disable-next-line */ 19 | font-size: 0.7em; 20 | text-decoration: none; 21 | margin-left: 1ch; 22 | } 23 | } 24 | 25 | .subsubtitle { 26 | @include style.subsubtitle-font; 27 | 28 | margin: 0; 29 | margin-top: 1rem; 30 | } 31 | -------------------------------------------------------------------------------- /components/Summary/Summary.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .container { 4 | display: grid; 5 | grid-template-columns: 1fr; 6 | gap: 1rem; 7 | 8 | @media (min-width: style.$mobile-breakpoint) { 9 | grid-template-columns: 1fr 2fr; 10 | } 11 | } 12 | 13 | .callout { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | text-align: center; 19 | } 20 | 21 | .image { 22 | width: 100%; 23 | height: auto; 24 | transition: opacity 0.2s ease-in-out; 25 | 26 | &:hover { 27 | opacity: 0.75; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/Radio/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ComponentProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Radio.module.scss'; 4 | 5 | interface Props extends Omit, 'onChange'> { 6 | checked: boolean; 7 | onChange: (value: string) => void; 8 | } 9 | 10 | export const Radio: React.FC = ({ className, onChange, ...rest }) => ( 11 | ) => onChange(event.target.value)} 14 | className={classNames(styles['input'], className)} 15 | {...rest} 16 | /> 17 | ); 18 | -------------------------------------------------------------------------------- /components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { ComponentProps } from 'react'; 3 | import { LinkComponentFactory } from 'components/Link'; 4 | import styles from './Button.module.scss'; 5 | 6 | interface Props extends ComponentProps<'button'> { 7 | small?: boolean; 8 | } 9 | 10 | export const Button = ({ children, className, small, ...rest }: Props) => ( 11 | 17 | ); 18 | 19 | export const ButtonLink = LinkComponentFactory(styles['button']); 20 | -------------------------------------------------------------------------------- /components/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ComponentProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Checkbox.module.scss'; 4 | 5 | interface Props extends Omit, 'onChange'> { 6 | checked: boolean; 7 | onChange: (value: boolean) => void; 8 | } 9 | 10 | export const Checkbox: React.FC = ({ className, checked, onChange }) => ( 11 | ) => onChange(event.target.checked)} 14 | checked={checked} 15 | className={classNames(styles['input'], className)} 16 | /> 17 | ); 18 | -------------------------------------------------------------------------------- /app/(main)/Home.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | @use 'components/Editor/Editor.module.scss' as editor; 3 | 4 | .callout-wrapper { 5 | display: grid; 6 | grid-template-columns: 1fr; 7 | gap: 1rem; 8 | min-height: 8rem; 9 | 10 | @media (min-width: style.$mobile-breakpoint) { 11 | grid-template-columns: 1fr 1fr; 12 | } 13 | } 14 | 15 | .editor-wrapper { 16 | @include editor.editor-styles; 17 | } 18 | 19 | .solution-wrapper { 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | width: 100%; 24 | } 25 | 26 | .sponsor { 27 | height: 1.5rem; 28 | width: auto; 29 | margin-top: 1rem; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | - name: Setup node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version-file: '.nvmrc' 20 | - name: Install 21 | run: yarn install 22 | - name: Typescript check 23 | run: yarn lint:tsc 24 | - name: ESLint check 25 | run: yarn lint 26 | - name: Stylelint check 27 | run: yarn lint:style 28 | -------------------------------------------------------------------------------- /components/Exercise/Exercise.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .container { 4 | margin-bottom: 2rem; 5 | } 6 | 7 | .task { 8 | text-align: center; 9 | font-weight: bold; 10 | } 11 | 12 | .buttons-wrapper { 13 | width: 100%; 14 | display: flex; 15 | justify-content: center; 16 | flex-wrap: wrap; 17 | 18 | > * { 19 | width: 100%; 20 | @media (min-width: style.$mobile-breakpoint) { 21 | width: auto; 22 | } 23 | } 24 | 25 | > :not(:last-child) { 26 | margin-bottom: 1rem; 27 | margin-right: 0; 28 | @media (min-width: style.$mobile-breakpoint) { 29 | margin-bottom: 0; 30 | margin-right: 1rem; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Image from 'next/image'; 4 | import { PAGES } from 'services/pages'; 5 | import styles from './Header.module.scss'; 6 | import logoWhiteImage from './logo-white.png'; 7 | 8 | export const Header: React.FC = () => ( 9 |
10 |

11 | 16 | css dojo 17 | 18 | (re)learn CSS, the right way 19 | 20 |

21 |
22 | ); 23 | -------------------------------------------------------------------------------- /app/(dojo)/dojo/[kataId]/Dojo.module.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | .dojo { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | align-items: center; 8 | justify-content: space-around; 9 | min-height: calc(100vh - 2 * style.$main-vertical-margin - 2 * style.$main-vertical-padding); 10 | } 11 | 12 | .title { 13 | text-align: center; 14 | } 15 | 16 | .additional-informations { 17 | margin: 1rem 0; 18 | } 19 | 20 | .buttons-wrapper { 21 | display: flex; 22 | flex-wrap: wrap; 23 | 24 | > * { 25 | width: 100%; 26 | margin-bottom: 1rem; 27 | } 28 | 29 | @media (min-width: style.$mobile-breakpoint) { 30 | > * { 31 | width: auto; 32 | margin-bottom: 0; 33 | } 34 | 35 | > :not(:last-child) { 36 | margin-right: 1rem; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['stylelint-declaration-strict-value'], 3 | extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier'], 4 | rules: { 5 | 'declaration-no-important': true, 6 | 'selector-max-id': 0, 7 | 'selector-max-class': 2, 8 | 'selector-max-type': 0, 9 | 'scale-unlimited/declaration-strict-value': [ 10 | [ 11 | 'font-size', 12 | 'line-height', 13 | 'z-index', 14 | 'color', 15 | 'background-color', 16 | 'border-color', 17 | 'border-top-color', 18 | 'border-right-color', 19 | 'border-bottom-color', 20 | 'border-left-color', 21 | 'fill', 22 | ], 23 | { 24 | ignoreKeywords: ['transparent', 'inherit', 'initial', 'unset', 'none', 'currentColor'], 25 | }, 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /app/(main)/global.scss: -------------------------------------------------------------------------------- 1 | @use 'stylesheet.scss' as style; 2 | 3 | // We are resetting base HTML styles, using HTML tags is OK here 4 | // stylelint-disable 5 | 6 | @font-face { 7 | font-family: 'Virgil'; 8 | src: url('https://excalidraw.com/Virgil.woff2'); 9 | } 10 | 11 | /* HTML5 display-role reset for older browsers */ 12 | article, 13 | aside, 14 | details, 15 | figcaption, 16 | figure, 17 | footer, 18 | header, 19 | hgroup, 20 | menu, 21 | nav, 22 | section, 23 | main { 24 | display: block; 25 | } 26 | 27 | html { 28 | height: 100vh; 29 | color: style.$darkGrey; 30 | padding: 0; 31 | margin: 0; 32 | 33 | @include style.body-font; 34 | } 35 | 36 | body, 37 | #__next { 38 | height: 100%; 39 | padding: 0; 40 | margin: 0; 41 | } 42 | 43 | input::-ms-clear { 44 | display: none; 45 | height: 0; 46 | width: 0; 47 | } 48 | 49 | dl { 50 | margin: 0; 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | CSS Dojo 4 | 5 | 6 |

7 | (re)learn CSS, the right way! 8 |
9 | Go to the website » 10 |

11 |
12 | 13 | 14 | ## About 15 | 16 | I know, I know... you may think that [**C**SS **S**ometimes **S**ucks](https://talks.codemotion.com/why-css-sometimes-sucks). But it's not a fatality. 17 | 18 | There is a alternate future, a parallel universe, in which this is not the case. 19 | 20 | cssdojo is the place to learn the concepts that will allow you to have fun with CSS and face any CSS issue with confidence. 21 | 22 | **[Go to the website »](https://cssdojo.dev)** 23 | 24 | ## Sponsors 25 | 26 | Theodo 27 | -------------------------------------------------------------------------------- /stylesheet.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable 2 | 3 | // Colors 4 | $black: #000000; 5 | $darkGrey: #414141; 6 | $lightGrey: #dcdcdc; 7 | $lighterGrey: #efefef; 8 | $white: #ffffff; 9 | $blue: #278bca; 10 | 11 | // Breakpoints 12 | $mobile-breakpoint: 50em; 13 | 14 | // Sizing 15 | $page-width: 40rem; 16 | $main-vertical-margin: 1rem; 17 | $main-vertical-padding: 1rem; 18 | 19 | // Typography 20 | @mixin body-font() { 21 | font-family: var(--font-main), sans-serif; 22 | font-size: 125%; 23 | line-height: 1.5; 24 | } 25 | 26 | @mixin title-font() { 27 | font-size: 1.75rem; 28 | line-height: 1.5; 29 | } 30 | 31 | @mixin subtitle-font() { 32 | font-size: 1.25rem; 33 | line-height: 1.5; 34 | } 35 | 36 | @mixin subsubtitle-font() { 37 | font-size: 1.15rem; 38 | line-height: 1.5; 39 | } 40 | 41 | @mixin code-font() { 42 | font-size: 0.8em; 43 | font-family: monospace; 44 | } 45 | 46 | @mixin small-font() { 47 | font-size: 0.8rem; 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "baseUrl": ".", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "sourceMap": true, 22 | "alwaysStrict": true, 23 | "incremental": true, 24 | "plugins": [ 25 | { 26 | "name": "next" 27 | } 28 | ] 29 | }, 30 | "exclude": [ 31 | ".next", 32 | "next.config.js" 33 | ], 34 | "include": [ 35 | ".eslintrc.js", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | "*/**.js", 39 | "*.js", 40 | ".next/types/**/*.ts" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /components/KataQuestions/index.tsx: -------------------------------------------------------------------------------- 1 | import { Skill } from 'components/Skill'; 2 | import { Divider } from 'components/Divider'; 3 | import { SKILLS } from 'services/skills'; 4 | import { Link } from 'components/Link'; 5 | import { PAGES } from 'services/pages'; 6 | import styles from './KataQuestions.module.scss'; 7 | 8 | interface Props { 9 | skillIds: (keyof typeof SKILLS)[]; 10 | } 11 | 12 | export const KataQuestions = ({ skillIds }: Props) => ( 13 | <> 14 |

15 | In this kata, we’re going to answer the following questions: 16 | 17 | {skillIds.map((skillId) => ( 18 | 19 | 20 | 21 | ))} 22 | 23 | 24 | (link to the complete list of questions) 25 | 26 |

27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ComponentProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Input.module.scss'; 4 | 5 | interface Props extends Omit, 'onChange'> { 6 | onChange: (value: string) => void; 7 | } 8 | 9 | export const Input: React.FC = ({ className, onChange, ...rest }) => ( 10 | ) => onChange(event.target.value)} 12 | className={classNames(styles['input'], className)} 13 | {...rest} 14 | /> 15 | ); 16 | 17 | interface TextareaProps extends Omit, 'onChange'> { 18 | onChange: (value: string) => void; 19 | } 20 | 21 | export const Textarea: React.FC = ({ className, onChange, ...rest }) => ( 22 |