├── .env.example ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── BadgesList.yml │ ├── ThemeList.yml │ └── monkeytype-readme.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── action.yml ├── app.js ├── monkeytype-data ├── badges.json └── themes.json ├── package-lock.json ├── package.json ├── public ├── assets │ ├── robots.txt │ └── sitemap.xml ├── image │ ├── apeMonkeyIcon.png │ ├── github-30*30.svg │ ├── github.png │ ├── github.svg │ ├── github.webp │ ├── plus-solid.svg │ ├── stupidMonkeyIcon-30*30.svg │ ├── stupidMonkeyIcon.png │ ├── stupidMonkeyIcon.svg │ ├── stupidMonkeyIcon.webp │ ├── stupidMonkeyIconWhite.png │ ├── stupidMonkeyIconWhite.webp │ └── userImg │ │ └── README.md ├── script │ ├── generateSvg.js │ ├── index.js │ ├── monkeytypeData.js │ ├── prism.js │ ├── tailwindCSS.js │ └── user.js ├── style │ ├── input.css │ ├── output.css │ └── prism.css └── views │ ├── favicon.ejs │ ├── index.ejs │ ├── logo.ejs │ └── user.ejs └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | DOMAIN=your-domain -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: 9 | - "dependencies" 10 | 11 | # Maintain dependencies for yarn 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | labels: 17 | - "dependencies" 18 | -------------------------------------------------------------------------------- /.github/workflows/BadgesList.yml: -------------------------------------------------------------------------------- 1 | name: Update monkeytype badges json list 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */24 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | make-request: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: "20.x" 19 | 20 | - name: Get badge json data from monkeytype 21 | run: curl -o monkeytype-data/badges.json https://monkeytype-readme.zeabur.app/mr-command/badge 22 | 23 | - name: Format code with Prettier 24 | run: npx prettier --write ./monkeytype-data/badges.json 25 | 26 | - name: Print json file contents 27 | run: cat monkeytype-data/badges.json 28 | 29 | - name: Update badge list 30 | uses: actions/github-script@v7 31 | with: 32 | script: | 33 | const fs = require('fs'); 34 | let monkeytypeBadgesJson = fs.readFileSync('monkeytype-data/badges.json'); 35 | const filePath = '${{ github.workspace }}/monkeytype-data/badges.json'; 36 | fs.writeFileSync(filePath, monkeytypeBadgesJson); 37 | 38 | - name: Configure git 39 | run: | 40 | git config --global user.name "${{ vars.GITUSERNAME }}" 41 | git config --global user.email "${{ vars.GITUSEREMAIL }}" 42 | 43 | - name: Commit and push changes 44 | run: | 45 | if git diff-index --quiet HEAD --; then 46 | echo "No changes detected." 47 | else 48 | git add . 49 | git commit -m "chore: update badges json list" 50 | fi 51 | continue-on-error: true 52 | 53 | - name: Push changes 54 | uses: ad-m/github-push-action@master 55 | with: 56 | branch: master 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | -------------------------------------------------------------------------------- /.github/workflows/ThemeList.yml: -------------------------------------------------------------------------------- 1 | name: Update monkeytype theme json list 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */24 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | make-request: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: "20.x" 19 | 20 | - name: Download theme json file from monkeytype 21 | run: wget --timeout=180 --tries=3 --waitretry=10 --output-document=monkeytype-data/themes.json https://monkeytype-readme.zeabur.app/mr-command/theme 22 | timeout-minutes: 10 23 | 24 | - name: Format JSON data 25 | run: | 26 | python3 -c 'import json; data = json.load(open("monkeytype-data/themes.json")); json.dump(data, open("monkeytype-data/themes.json", "w"), indent=2)' 27 | 28 | - name: Format code with Prettier 29 | run: npx prettier --write ./monkeytype-data/themes.json 30 | 31 | - name: Print json file contents 32 | run: cat monkeytype-data/themes.json 33 | 34 | - name: Update theme list 35 | uses: actions/github-script@v7 36 | with: 37 | script: | 38 | const fs = require('fs'); 39 | let monkeytypeThemesJson = fs.readFileSync('monkeytype-data/themes.json'); 40 | const filePath = '${{ github.workspace }}/monkeytype-data/themes.json'; 41 | fs.writeFileSync(filePath, monkeytypeThemesJson); 42 | 43 | - name: Print updated theme contents 44 | run: | 45 | cat monkeytype-data/themes.json 46 | 47 | - name: Configure git 48 | run: | 49 | git config --global user.name "${{ vars.GITUSERNAME }}" 50 | git config --global user.email "${{ vars.GITUSEREMAIL }}" 51 | 52 | - name: Commit and push changes 53 | run: | 54 | if git diff-index --quiet HEAD --; then 55 | echo "No changes detected." 56 | else 57 | git add . 58 | git commit -m "chore: update themes json list" 59 | fi 60 | continue-on-error: true 61 | 62 | - name: Push changes 63 | uses: ad-m/github-push-action@master 64 | with: 65 | branch: master 66 | github_token: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /.github/workflows/monkeytype-readme.yml: -------------------------------------------------------------------------------- 1 | name: generate monkeytype readme svg 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */24 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | download-svg: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: "20.x" 19 | 20 | - name: Download SVG 21 | run: | 22 | mkdir monkeytype-readme-svg 23 | curl -o monkeytype-readme-svg/monkeytype-readme.svg https://monkeytype-readme.zeabur.app/generate-svg/rocket/botanical 24 | curl -o monkeytype-readme-svg/monkeytype-readme-lb.svg https://monkeytype-readme.zeabur.app/generate-svg/rocket/botanical?lb=true 25 | curl -o monkeytype-readme-svg/monkeytype-readme-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/rocket/botanical?pb=true 26 | curl -o monkeytype-readme-svg/monkeytype-readme-lb-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/rocket/botanical?lbpb=true 27 | 28 | curl -o monkeytype-readme-svg/monkeytype-readme-Miodec.svg https://monkeytype-readme.zeabur.app/generate-svg/Miodec/nord_light 29 | curl -o monkeytype-readme-svg/monkeytype-readme-Miodec-lb.svg https://monkeytype-readme.zeabur.app/generate-svg/Miodec/nord_light?lb=true 30 | curl -o monkeytype-readme-svg/monkeytype-readme-Miodec-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/Miodec/nord_light?pb=true 31 | curl -o monkeytype-readme-svg/monkeytype-readme-Miodec-lb-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/Miodec/nord_light?lbpb=true 32 | 33 | curl -o monkeytype-readme-svg/monkeytype-readme-rocket-slambook-lb-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/rocket/slambook?lbpb=true 34 | 35 | curl -o monkeytype-readme-svg/monkeytype-readme-UTF8.svg https://monkeytype-readme.zeabur.app/generate-svg/UTF8/camping 36 | 37 | curl -o monkeytype-readme-svg/monkeytype-readme-ridemountainpig.svg https://monkeytype-readme.zeabur.app/generate-svg/ridemountainpig/witch_girl 38 | 39 | curl -o monkeytype-readme-svg/monkeytype-readme-semi.svg https://monkeytype-readme.zeabur.app/generate-svg/semi/blueberry_light 40 | 41 | curl -o monkeytype-readme-svg/monkeytype-readme-mac-lb.svg https://monkeytype-readme.zeabur.app/generate-svg/mac/mizu?lb=true 42 | 43 | curl -o monkeytype-readme-svg/monkeytype-readme-ze_or.svg https://monkeytype-readme.zeabur.app/generate-svg/ze_or/darling 44 | 45 | curl -o monkeytype-readme-svg/monkeytype-readme-nask-lb.svg https://monkeytype-readme.zeabur.app/generate-svg/nask/beach?lb=true 46 | 47 | curl -o monkeytype-readme-svg/monkeytype-readme-davidho0403-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/davidho0403/lil_dragon?pb=true 48 | 49 | - name: push monkeytype-readme.svg to the monkeytype-readme branch 50 | uses: crazy-max/ghaction-github-pages@v4.0.0 51 | with: 52 | target_branch: monkeytype-readme 53 | build_dir: monkeytype-readme-svg 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | public/image/userImg/* 4 | !public/image/userImg/README.md 5 | public/views/test.html 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/style/output.css -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # MonkeyType Readme 6 | 7 | Monkeytype Readme transforms MonkeyType typing data into SVGs for your GitHub Readme. 8 | 9 | 10 | My Monkeytype profile 11 | 12 | 13 | ## Usage 14 | 15 | Two way to generate MonkeyType Readme 16 | 17 | 1. #### Using GitHub Action from Marketplace 18 | 2. #### Setting Up Custom MonkeyType Readme GitHub Action 19 | 20 | ### Using GitHub Action from Marketplace ([MonkeyType Readme Github Action](https://github.com/marketplace/actions/monkeytype-readme)) 21 | 22 | 1. Add a `monkeytype-readme.yml` file in your repository's `.github/workflows/` path. 23 | 2. Configure `monkeytype-readme.yml` with the following format: 24 | 25 | - **Username**: Change `MONKEYTYPE_USERNAME` to your username in MonkeyType. 26 | 27 | - **Themes**: Change `MONKEYTYPE_THEME_NAME` to your favorite theme in MonkeyType.
28 | If theme name have `space`, please change `space` to `_`.
29 | 30 | > Example: `nord light` => `nord_light` 31 | 32 | - **Target Branch**: Change `BRANCH_NAME` to the branch you want to put MonkeyType Readme. 33 | 34 | ```yml 35 | name: generate monkeytype readme svg 36 | 37 | on: 38 | schedule: 39 | - cron: "0 */6 * * *" # every 6 hours 40 | workflow_dispatch: 41 | 42 | jobs: 43 | download-svg: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Checkout code 48 | uses: actions/checkout@v4 49 | 50 | - name: Set up Node.js 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: "20.x" 54 | 55 | - name: Generate Monkeytype Readme SVG 56 | uses: monkeytype-hub/monkeytype-readme@v1.0.0 57 | with: 58 | username: MONKEYTYPE_USERNAME 59 | themes: MONKEYTYPE_THEME_NAME 60 | target-branch: BRANCH_NAME 61 | github-token: ${{ secrets.GITHUB_TOKEN }} 62 | ``` 63 | 64 | 3. Go to actions and run `generate monkeytype readme svg` workflow. 65 | 66 | 4. Done! Now, navigate to your target branch and you'll find the MonkeyType README file. You can also integrate it into your GitHub README. 67 | 68 | ### Setting Up Custom MonkeyType Readme GitHub Action 69 | 70 | 1. Add a `monkeytype-readme.yml` file in your repository's `.github/workflows/` path. 71 | 2. Configure `monkeytype-readme.yml` with the following format: 72 | 73 | > Note: change YOUR_USERNAME to your MonkeyType username. 74 | 75 | > Note: This workflow will auto to update your MonkeyType Readme. 76 | 77 | #### Themes 78 | 79 | Change `THEMES` to your favorite theme in MonkeyType.
80 | If theme name have `space`, please change `space` to `_`.
81 | 82 | > Example: `nord light` => `nord_light` 83 | 84 | #### SVGs information 85 | 86 | 87 | My Monkeytype profile 88 | 89 | 90 | #### With LeaderBoards: `?lb=true` 91 | 92 | 93 | My Monkeytype profile 94 | 95 | 96 | #### With PersonalBests: `?pb=true` 97 | 98 | 99 | My Monkeytype profile 100 | 101 | 102 | #### With LeaderBoards and PersonalBests: `?lbpb=true` 103 | 104 | #### github actions 105 | 106 | ```yml 107 | name: generate monkeytype readme svg 108 | 109 | on: 110 | schedule: 111 | - cron: "0 */6 * * *" # every 6 hours 112 | workflow_dispatch: 113 | 114 | jobs: 115 | download-svg: 116 | runs-on: ubuntu-latest 117 | steps: 118 | - name: Checkout code 119 | uses: actions/checkout@v3 120 | 121 | - name: Set up Node.js 122 | uses: actions/setup-node@v3 123 | with: 124 | node-version: "20.x" 125 | 126 | - name: Download SVG 127 | run: | 128 | mkdir public 129 | curl -o public/monkeytype-readme.svg https://monkeytype-readme.zeabur.app/generate-svg/YOUR_USERNAME/THEMES 130 | curl -o public/monkeytype-readme-lb.svg https://monkeytype-readme.zeabur.app/generate-svg/YOUR_USERNAME/THEMES?lb=true 131 | curl -o public/monkeytype-readme-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/YOUR_USERNAME/THEMES?pb=true 132 | curl -o public/monkeytype-readme-lb-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/YOUR_USERNAME/THEMES?lbpb=true 133 | 134 | - name: push monkeytype-readme.svg to the monkeytype-readme branch 135 | uses: crazy-max/ghaction-github-pages@v4.0.0 136 | with: 137 | target_branch: monkeytype-readme 138 | build_dir: public 139 | env: 140 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 141 | ``` 142 | 143 | 3. Add SVGs to your GitHub Readme. 144 | 145 | > Note:
146 | > change YOUR_MONKEYTYPE_USERNAME to your MonkeyType username.
147 | > change YOUR_GITHUB_USERNAME to your Github username.
148 | > change YOUR_GITHUB_REPOSITORY to your repository name.
149 | > change SVG_NAME to the svg you want to use.
150 | > 151 | > > original : monkeytype-readme.svg
152 | > > original + leader boards : monkeytype-readme-lb.svg
153 | > > original + personal bests : monkeytype-readme-pb.svg
154 | > > original + leader boards + personal bests : monkeytype-readme-lbpb.svg 155 | 156 | ```md 157 | 158 | My Monkeytype profile 159 | 160 | ``` 161 | 162 | 4. Go to actions and run `generate monkeytype readme svg` workflow. 163 | 164 | 5. Done! Your MonkeyType Readme will show on your Readme. 165 | 166 | ## Running Locally 167 | 168 | To run MonkeyType Readme locally, follow these steps: 169 | 170 | 1. Clone this repository: 171 | 172 | ```bash 173 | git clone https://github.com/monkeytype-hub/monkeytype-readme.git 174 | ``` 175 | 176 | 2. Store the MonkeyType APE keys in `.env`: 177 | 178 | ```bash 179 | cp .env.example .env 180 | ``` 181 | 182 | 3. Install the dependencies: 183 | 184 | ```bash 185 | npm install 186 | ``` 187 | 188 | 4. Run the application: 189 | 190 | ```bash 191 | npm run dev 192 | ``` 193 | 194 | 5. Finally, visit [http://localhost:3000](http://localhost:3000/) in your web browser. 195 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Monkeytype Readme" 2 | description: "A GitHub Action to generate Monkeytype Readme SVGs, let you share Monkeytype story with the world." 3 | branding: 4 | icon: "bookmark" 5 | color: "yellow" 6 | inputs: 7 | username: 8 | description: "Your Monkeytype username" 9 | required: true 10 | themes: 11 | description: "Themes to generate SVGs for" 12 | required: true 13 | target-branch: 14 | description: "Branch to deploy Monkeytype Readme SVGs" 15 | required: true 16 | github-token: 17 | description: "GitHub token use to deploy github pages" 18 | required: true 19 | 20 | runs: 21 | using: "composite" 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Set up Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: "20.x" 30 | 31 | - name: Download SVG 32 | shell: bash 33 | run: | 34 | mkdir monkeytype-readme-svg 35 | curl -o monkeytype-readme-svg/${{ inputs.username }}-monkeytype-readme.svg https://monkeytype-readme.zeabur.app/generate-svg/${{ inputs.username }}/${{ inputs.themes }} 36 | curl -o monkeytype-readme-svg/${{ inputs.username }}-monkeytype-readme-lb.svg https://monkeytype-readme.zeabur.app/generate-svg/${{ inputs.username }}/${{ inputs.themes }}?lb=true 37 | curl -o monkeytype-readme-svg/${{ inputs.username }}-monkeytype-readme-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/${{ inputs.username }}/${{ inputs.themes }}?pb=true 38 | curl -o monkeytype-readme-svg/${{ inputs.username }}-monkeytype-readme-lb-pb.svg https://monkeytype-readme.zeabur.app/generate-svg/${{ inputs.username }}/${{ inputs.themes }}?lbpb=true 39 | 40 | - name: push monkeytype-readme.svg to the monkeytype-readme branch 41 | uses: crazy-max/ghaction-github-pages@v4.0.0 42 | with: 43 | target_branch: ${{ inputs.target-branch }} 44 | build_dir: monkeytype-readme-svg 45 | env: 46 | GITHUB_TOKEN: ${{ inputs.github-token }} 47 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const path = require("path"); 4 | const axios = require("axios"); 5 | const app = express(); 6 | 7 | const { 8 | getTheme, 9 | getFaviconTheme, 10 | getBadge, 11 | getUserData, 12 | getMonkeyTypeThemesData, 13 | getMonkeyTypeBadgesData, 14 | } = require("./public/script/monkeytypeData"); 15 | const { getOGSvg, getSvg } = require("./public/script/generateSvg"); 16 | require("dotenv").config(); 17 | 18 | app.use(compression()); 19 | app.use(express.static("public")); 20 | app.use("/styles", express.static("dist")); 21 | 22 | app.set("view engine", "ejs"); 23 | app.set("views", path.join(__dirname, "public", "views")); 24 | 25 | app.use("/node_modules", express.static(path.join(__dirname, "node_modules"))); 26 | 27 | app.get("/", (req, res) => { 28 | const data = { 29 | domain: process.env.DOMAIN, 30 | }; 31 | 32 | res.render("index", { data }); 33 | }); 34 | 35 | app.get("/mr-command/theme", async (req, res) => { 36 | try { 37 | const themesData = await getMonkeyTypeThemesData(); 38 | res.set("Content-Type", "application/json"); 39 | res.send(themesData); 40 | } catch (err) { 41 | console.error("Failed to fetch themes:", err); 42 | res.status(500).json({ error: "Failed to fetch themes" }); 43 | } 44 | }); 45 | 46 | app.get("/mr-command/badge", async (req, res) => { 47 | try { 48 | const badgesData = await getMonkeyTypeBadgesData(); 49 | res.set("Content-Type", "application/json"); 50 | res.send(badgesData); 51 | } catch (err) { 52 | console.error("Failed to fetch badges:", err); 53 | res.status(500).json({ error: "Failed to fetch badges" }); 54 | } 55 | }); 56 | 57 | app.get("/mr-command/favicon", async (req, res) => { 58 | try { 59 | const faviconData = getFaviconTheme(); 60 | res.render("favicon", { faviconData }); 61 | } catch (err) { 62 | console.error("Failed to render favicon:", err); 63 | res.status(500).send("Failed to render favicon"); 64 | } 65 | }); 66 | 67 | app.get("/mr-command/logo", async (req, res) => { 68 | try { 69 | const faviconData = getFaviconTheme(); 70 | res.render("logo", { faviconData }); 71 | } catch (err) { 72 | console.error("Failed to render logo:", err); 73 | res.status(500).send("Failed to render logo"); 74 | } 75 | }); 76 | 77 | app.get("/sitemap.xml", (req, res) => { 78 | res.sendFile(path.join(__dirname, "public/assets", "sitemap.xml")); 79 | }); 80 | 81 | app.get("/robots.txt", (req, res) => { 82 | res.sendFile(path.join(__dirname, "public/assets", "robots.txt")); 83 | }); 84 | 85 | app.get( 86 | ["/og-image/:userId/:themeName", "/og-image/:userId"], 87 | async (req, res) => { 88 | const userId = req.params.userId; 89 | const themeName = req.params.themeName; 90 | req.query.lbpb == "true" ? (leaderBoards = personalBests = true) : null; 91 | const userData = await getUserData(userId); 92 | const theme = getTheme(themeName); 93 | if (userData === undefined || userData.name === undefined) { 94 | const svg = await getOGSvg(null, theme, null); 95 | res.set("Content-Type", "image/svg+xml"); 96 | res.send(svg); 97 | return; 98 | } 99 | let badge = null; 100 | if (userData.inventory !== null && userData.inventory !== undefined) { 101 | if (userData.inventory.badges.length !== 0) { 102 | badge = getBadge(userData.inventory.badges[0].id); 103 | for (let i = 0; i < userData.inventory.badges.length; i++) { 104 | if (userData.inventory.badges[i].selected === true) { 105 | badge = getBadge(userData.inventory.badges[i].id); 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | const ogSvg = await getOGSvg(userData, theme, badge); 112 | 113 | try { 114 | const response = await axios.post( 115 | "https://mr-api.zeabur.app/og-image", 116 | { og_svg: ogSvg }, 117 | { responseType: "arraybuffer" }, 118 | ); 119 | 120 | res.setHeader("Content-Type", "image/png"); 121 | res.send(Buffer.from(response.data, "binary")); 122 | } catch (error) { 123 | console.error("Error converting SVG to PNG:", error); 124 | res.status(500).send("Error converting SVG to PNG"); 125 | } 126 | }, 127 | ); 128 | 129 | app.get(["/:userId/:themeName", "/:userId"], async (req, res) => { 130 | const data = { 131 | domain: process.env.DOMAIN, 132 | userId: req.params.userId, 133 | theme: getTheme( 134 | req.params.themeName ? req.params.themeName : "serika_dark", 135 | ), 136 | userData: await getUserData(req.params.userId), 137 | svgUrl: `${process.env.DOMAIN}/generate-svg/${req.params.userId}/${req.params.themeName}?lbpb=true`, 138 | }; 139 | 140 | res.render("user", { data }); 141 | }); 142 | 143 | app.get("/generate-svg/:userId/:themeName", async (req, res) => { 144 | const userId = req.params.userId; 145 | const themeName = req.params.themeName; 146 | let leaderBoards = req.query.lb == "true" ? true : false; 147 | let personalBests = req.query.pb == "true" ? true : false; 148 | req.query.lbpb == "true" ? (leaderBoards = personalBests = true) : null; 149 | const userData = await getUserData(userId); 150 | const theme = getTheme(themeName); 151 | if (userData === undefined || userData.name === undefined) { 152 | const svg = await getSvg(null, theme, null, false, false); 153 | res.set("Content-Type", "image/svg+xml"); 154 | res.send(svg); 155 | return; 156 | } 157 | let badge = null; 158 | if (userData.inventory !== null && userData.inventory !== undefined) { 159 | if (userData.inventory.badges.length !== 0) { 160 | badge = getBadge(userData.inventory.badges[0].id); 161 | for (let i = 0; i < userData.inventory.badges.length; i++) { 162 | if (userData.inventory.badges[i].selected === true) { 163 | badge = getBadge(userData.inventory.badges[i].id); 164 | break; 165 | } 166 | } 167 | } 168 | } 169 | const svg = await getSvg( 170 | userData, 171 | theme, 172 | badge, 173 | leaderBoards, 174 | personalBests, 175 | ); 176 | res.set("Content-Type", "image/svg+xml"); 177 | res.send(svg); 178 | }); 179 | 180 | app.listen(process.env.PORT || 3000, async () => { 181 | console.log("Server started on port " + (process.env.PORT || 3000)); 182 | }); 183 | -------------------------------------------------------------------------------- /monkeytype-data/badges.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "id": 1, 4 | "name": "Developer", 5 | "description": "I made this", 6 | "icon": "fa-laptop", 7 | "color": "white", 8 | "customStyle": "animation: rgb-bg 10s linear infinite; background: linear-gradient(45deg in hsl longer hue, hsl(330, 90%, 30%) 0%, hsl(250, 90%, 30%) 100%);", 9 | "iconSvg": "" 10 | }, 11 | "2": { 12 | "id": 2, 13 | "name": "Collaborator", 14 | "description": "I helped make this", 15 | "icon": "fa-code", 16 | "color": "white", 17 | "customStyle": "animation: rgb-bg 10s linear infinite; background: linear-gradient(45deg in hsl longer hue, hsl(330, 90%, 30%) 0%, hsl(250, 90%, 30%) 100%);", 18 | "iconSvg": "" 19 | }, 20 | "3": { 21 | "id": 3, 22 | "name": "Server Mod", 23 | "description": "Discord server moderator", 24 | "icon": "fa-hammer", 25 | "color": "white", 26 | "customStyle": "animation: rgb-bg 10s linear infinite; background: linear-gradient(45deg in hsl longer hue, hsl(330, 90%, 30%) 0%, hsl(250, 90%, 30%) 100%);", 27 | "iconSvg": "" 28 | }, 29 | "4": { 30 | "id": 4, 31 | "name": "OG Account", 32 | "description": "First 1000 users on the site", 33 | "icon": "fa-baby", 34 | "color": "bgColor", 35 | "background": "mainColor", 36 | "iconSvg": "" 37 | }, 38 | "5": { 39 | "id": 5, 40 | "name": "OG Discordian", 41 | "description": "First 1000 Discord server members", 42 | "icon": "fa-baby", 43 | "color": "bgColor", 44 | "background": "mainColor", 45 | "iconSvg": "" 46 | }, 47 | "6": { 48 | "id": 6, 49 | "name": "Supporter", 50 | "description": "Donated money", 51 | "icon": "fa-heart", 52 | "color": "textColor", 53 | "background": "subColor", 54 | "iconSvg": "" 55 | }, 56 | "7": { 57 | "id": 7, 58 | "name": "Sugar Daddy", 59 | "description": "Donated a lot of money", 60 | "icon": "fa-gem", 61 | "color": "bgColor", 62 | "background": "mainColor", 63 | "iconSvg": "" 64 | }, 65 | "8": { 66 | "id": 8, 67 | "name": "Monkey Supporter", 68 | "description": "Donated more money", 69 | "icon": "fa-heart", 70 | "color": "bgColor", 71 | "background": "mainColor", 72 | "iconSvg": "" 73 | }, 74 | "9": { 75 | "id": 9, 76 | "name": "White Hat", 77 | "description": "Reported critical vulnerabilities on the site", 78 | "icon": "fa-user-secret", 79 | "color": "bgColor", 80 | "background": "mainColor", 81 | "iconSvg": "" 82 | }, 83 | "10": { 84 | "id": 10, 85 | "name": "Bug Hunter", 86 | "description": "Reported or helped track down bugs on the site", 87 | "icon": "fa-bug", 88 | "color": "textColor", 89 | "background": "subColor", 90 | "iconSvg": "" 91 | }, 92 | "11": { 93 | "id": 11, 94 | "name": "Content Creator", 95 | "description": "Verified content creator", 96 | "icon": "fa-video", 97 | "color": "textColor", 98 | "background": "subColor", 99 | "iconSvg": "" 100 | }, 101 | "12": { 102 | "id": 12, 103 | "name": "Contributor", 104 | "description": "Contributed to the site", 105 | "icon": "fa-hands-helping", 106 | "color": "textColor", 107 | "background": "subColor", 108 | "iconSvg": "" 109 | }, 110 | "13": { 111 | "id": 13, 112 | "name": "Mythical", 113 | "description": "Yes, I'm actually this fast", 114 | "icon": "fa-rocket", 115 | "color": "white", 116 | "customStyle": "animation: rgb-bg 10s linear infinite; background: linear-gradient(45deg in hsl longer hue, hsl(330, 90%, 30%) 0%, hsl(250, 90%, 30%) 100%);", 117 | "iconSvg": "" 118 | }, 119 | "14": { 120 | "id": 14, 121 | "name": "All Year Long", 122 | "description": "Reached a streak of 365 days", 123 | "icon": "fa-fire", 124 | "color": "bgColor", 125 | "background": "mainColor", 126 | "iconSvg": "" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monkeytype-readme", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js", 9 | "dev": "nodemon app.js", 10 | "format": "prettier --write .", 11 | "tailwind": "tailwindcss -i ./public/style/input.css -o ./public/style/output.css --watch" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 17 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 18 | "@fortawesome/free-regular-svg-icons": "^6.4.0", 19 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 20 | "axios": "^1.6.8", 21 | "bootstrap-icons": "^1.11.1", 22 | "compression": "^1.8.0", 23 | "dotenv": "^16.5.0", 24 | "ejs": "^3.1.9", 25 | "express": "^4.18.2", 26 | "node-fetch": "^3.3.2", 27 | "request": "^2.88.2", 28 | "tailwindcss-animated": "^1.0.1" 29 | }, 30 | "devDependencies": { 31 | "nodemon": "^2.0.22", 32 | "prettier": "^3.0.3", 33 | "prettier-plugin-tailwindcss": "^0.2.7", 34 | "tailwindcss": "^3.3.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /generate-svg/YOUR_USERNAME/THEMES 4 | Sitemap: https://monkeytype-readme.zeabur.app/sitemap.xml 5 | -------------------------------------------------------------------------------- /public/assets/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | https://monkeytype-readme.com/ 11 | 2023-11-30T09:31:29+00:00 12 | 1.00 13 | 14 | 15 | https://monkeytype-readme.com/miodec/nord_light 16 | 2023-11-30T09:31:29+00:00 17 | 0.80 18 | 19 | 20 | https://monkeytype-readme.com/rocket/slambook 21 | 2023-11-30T09:31:29+00:00 22 | 0.80 23 | 24 | 25 | https://monkeytype-readme.com/UTF8/camping 26 | 2023-11-30T09:31:29+00:00 27 | 0.80 28 | 29 | 30 | https://monkeytype-readme.com/ridemountainpig/witch_girl 31 | 2023-11-30T09:31:29+00:00 32 | 0.80 33 | 34 | 35 | https://monkeytype-readme.com/semi/blueberry_light 36 | 2023-11-30T09:31:29+00:00 37 | 0.80 38 | 39 | 40 | https://monkeytype-readme.com/mac/mizu 41 | 2023-11-30T09:31:29+00:00 42 | 0.80 43 | 44 | 45 | https://monkeytype-readme.com/ze_or/darling 46 | 2023-11-30T09:31:29+00:00 47 | 0.80 48 | 49 | 50 | https://monkeytype-readme.com/nask/beach 51 | 2023-11-30T09:31:29+00:00 52 | 0.80 53 | 54 | 55 | https://monkeytype-readme.com/davidho0403/lil_dragon 56 | 2023-11-30T09:31:29+00:00 57 | 0.80 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/image/apeMonkeyIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/apeMonkeyIcon.png -------------------------------------------------------------------------------- /public/image/github-30*30.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/image/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/github.png -------------------------------------------------------------------------------- /public/image/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/image/github.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/github.webp -------------------------------------------------------------------------------- /public/image/plus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/image/stupidMonkeyIcon-30*30.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/image/stupidMonkeyIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/stupidMonkeyIcon.png -------------------------------------------------------------------------------- /public/image/stupidMonkeyIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/image/stupidMonkeyIcon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/stupidMonkeyIcon.webp -------------------------------------------------------------------------------- /public/image/stupidMonkeyIconWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/stupidMonkeyIconWhite.png -------------------------------------------------------------------------------- /public/image/stupidMonkeyIconWhite.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/6c105458564d3183f288b42674195cff794f721b/public/image/stupidMonkeyIconWhite.webp -------------------------------------------------------------------------------- /public/image/userImg/README.md: -------------------------------------------------------------------------------- 1 | ### This file is use to save the user images. Do not edit this file directly. 2 | -------------------------------------------------------------------------------- /public/script/generateSvg.js: -------------------------------------------------------------------------------- 1 | const { getOutputCSS } = require("./tailwindCSS"); 2 | 3 | const request = require("request"); 4 | const fs = require("fs"); 5 | 6 | const downloadUserImg = (url, path) => { 7 | return new Promise((resolve, reject) => { 8 | request.head(url, (err, res, body) => { 9 | request(url) 10 | .pipe(fs.createWriteStream(path)) 11 | .on("close", resolve) 12 | .on("error", reject); 13 | }); 14 | }); 15 | }; 16 | 17 | const getUserImg = async (userData, theme) => { 18 | let userImg; 19 | let defaultUserImg = ` 20 |
21 | 22 | 24 | 25 |
26 | `; 27 | if ( 28 | userData === null || 29 | userData.discordId === undefined || 30 | userData.discordAvatar === undefined 31 | ) { 32 | userImg = defaultUserImg; 33 | } else { 34 | // Download the image and save it to a file 35 | const imagePath = `public/image/userImg/${userData.discordId}-${userData.discordAvatar}.png`; 36 | await downloadUserImg( 37 | `https://cdn.discordapp.com/avatars/${userData.discordId}/${userData.discordAvatar}.png?size=256`, 38 | imagePath, 39 | ); 40 | 41 | // Convert the image file to base64 42 | const imageData = fs.readFileSync(imagePath); 43 | const base64Image = imageData.toString("base64"); 44 | 45 | if (base64Image == "") { 46 | userImg = defaultUserImg; 47 | } else { 48 | userImg = ` 49 |
50 | 51 |
52 | `; 53 | } 54 | } 55 | return userImg; 56 | }; 57 | 58 | const getUserBadge = (badge, theme) => { 59 | let userBadge = ""; 60 | if (badge !== null) { 61 | let color; 62 | if (badge.color === "white") color = "white"; 63 | else color = theme[badge.color]; 64 | 65 | badge.iconSvg = badge.iconSvg.replace('fill=""', `fill="${color}"`); 66 | userBadge = ` 67 |
75 |
${badge.iconSvg}
76 |
77 | ${badge.name} 78 |
79 |
80 | `; 81 | } 82 | return userBadge; 83 | }; 84 | 85 | const formatTopPercentage = (lbRank) => { 86 | if (lbRank?.rank === undefined) return "-"; 87 | if (lbRank?.count === undefined) return "-"; 88 | if (lbRank.rank === 1) return "GOAT"; 89 | let percentage = (lbRank.rank / lbRank.count) * 100; 90 | let formattedPercentage = 91 | percentage % 1 === 0 ? percentage.toString() : percentage.toFixed(2); 92 | return "Top " + formattedPercentage + "%"; 93 | }; 94 | 95 | async function getOGSvg(userData, theme, badge) { 96 | const width = 500; 97 | const height = 200; 98 | const cssData = await getOutputCSS(); 99 | 100 | let userImg = await getUserImg(userData, theme); 101 | let userBadge = getUserBadge(badge, theme); 102 | 103 | const svg = ` 104 | 106 | 109 | 110 |
111 |
114 |
115 |
${userImg}
116 |
117 |
120 | ${ 121 | userData == null 122 | ? "user not found" 123 | : userData.name 124 | } 125 |
126 | ${ 127 | badge != null 128 | ? `
${userBadge}
` 129 | : `` 130 | } 131 | ${ 132 | userData == null 133 | ? "" 134 | : userData.streak > 0 135 | ? ` 136 |
137 | Current streak: ${userData.streak} days 138 |
139 | ` 140 | : `` 141 | } 142 |
143 |
144 |
145 |
146 |
147 |
148 | `; 149 | return svg; 150 | } 151 | 152 | async function getSvg(userData, theme, badge, leaderBoards, personalbests) { 153 | const width = 500; 154 | let height = 220; 155 | leaderBoards ? (height += 220) : (height += 0); 156 | personalbests ? (height += 440) : (height += 0); 157 | const cssData = await getOutputCSS(); 158 | 159 | let userImg = await getUserImg(userData, theme); 160 | let userBadge = getUserBadge(badge, theme); 161 | 162 | let leaderBoardHTML = ""; 163 | if (leaderBoards == true) { 164 | topPercentage15 = formatTopPercentage( 165 | userData.allTimeLbs.time["15"]["english"], 166 | ); 167 | topPercentage60 = formatTopPercentage( 168 | userData.allTimeLbs.time["60"]["english"], 169 | ); 170 | 171 | const ordinalNumber = (rank) => { 172 | if (rank === undefined || rank === null) return ""; 173 | if (rank % 10 === 1) return "st"; 174 | if (rank % 10 === 2) return "nd"; 175 | if (rank % 10 === 3) return "rd"; 176 | return "th"; 177 | }; 178 | 179 | const allTimeLbs = userData.allTimeLbs; 180 | let rank15 = "-"; 181 | let rank60 = "-"; 182 | let ordinalNumber15 = ""; 183 | let ordinalNumber60 = ""; 184 | 185 | try { 186 | const time15 = allTimeLbs.time["15"] || {}; 187 | const time60 = allTimeLbs.time["60"] || {}; 188 | 189 | rank15 = !time15.english?.rank ? "-" : time15.english.rank; 190 | rank60 = !time60.english?.rank ? "-" : time60.english.rank; 191 | 192 | ordinalNumber15 = 193 | typeof time15.english?.rank === "number" 194 | ? ordinalNumber(time15.english.rank) 195 | : "-"; 196 | 197 | ordinalNumber60 = 198 | typeof time60.english?.rank === "number" 199 | ? ordinalNumber(time60.english.rank) 200 | : "-"; 201 | } catch (e) { 202 | console.log(e); 203 | console.log(userData); 204 | console.log(userData.allTimeLbs.time); 205 | rank15 = "-"; 206 | rank60 = "-"; 207 | ordinalNumber15 = ""; 208 | ordinalNumber60 = ""; 209 | } 210 | 211 | leaderBoardHTML = ` 212 |
213 |
214 |
215 |
216 | All-Time English Leaderboards 217 |
218 |
219 |
220 |
221 |
222 |
224 | 15 seconds 225 |
226 |
228 | ${topPercentage15} 229 |
230 |
231 |
233 | ${rank15} 234 |
235 |
237 | ${ordinalNumber15} 238 |
239 |
240 |
241 |
242 |
244 | 60 seconds 245 |
246 |
248 | ${topPercentage60} 249 |
250 |
251 |
253 | ${rank60} 254 |
255 |
257 | ${ordinalNumber60} 258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 | `; 266 | } 267 | 268 | let personalbestsHTML = ""; 269 | if (personalbests == true) { 270 | let pbTime = {}; 271 | for (let j = 15; j <= 120; j *= 2) { 272 | let english_1k = true; 273 | let english = true; 274 | let english_1k_pb = null; 275 | let english_pb = null; 276 | if (userData.personalBests.time[j] != undefined) { 277 | for ( 278 | let i = 0; 279 | i < userData.personalBests.time[j].length; 280 | i++ 281 | ) { 282 | if ( 283 | userData.personalBests.time[j][i].language == 284 | "english_1k" && 285 | userData.personalBests.time[j][i].difficulty == 286 | "normal" && 287 | userData.personalBests.time[j][i].punctuation == 288 | false && 289 | english_1k == true 290 | ) { 291 | english_1k_pb = userData.personalBests.time[j][i]; 292 | english_1k = false; 293 | } 294 | if ( 295 | userData.personalBests.time[j][i].language == 296 | "english" && 297 | userData.personalBests.time[j][i].difficulty == 298 | "normal" && 299 | userData.personalBests.time[j][i].punctuation == 300 | false && 301 | english == true 302 | ) { 303 | english_pb = userData.personalBests.time[j][i]; 304 | english = false; 305 | } 306 | } 307 | if (english_1k_pb == null && english_pb == null) { 308 | pbTime[j] = { wpm: "-", acc: "-" }; 309 | } else if (english_1k_pb != null && english_pb == null) { 310 | pbTime[j] = english_1k_pb; 311 | } else if (english_1k_pb == null && english_pb != null) { 312 | pbTime[j] = english_pb; 313 | } else { 314 | if (english_1k_pb.wpm > english_pb.wpm) { 315 | pbTime[j] = english_1k_pb; 316 | } else { 317 | pbTime[j] = english_pb; 318 | } 319 | } 320 | } else { 321 | pbTime[j] = { wpm: "-", acc: "-" }; 322 | } 323 | if (pbTime[j].wpm != "-") { 324 | pbTime[j].wpm = Math.round(parseFloat(pbTime[j].wpm)); 325 | } 326 | if (pbTime[j].acc != "-") { 327 | if (pbTime[j].acc == null || pbTime[j].acc == undefined) { 328 | pbTime[j].acc = "-"; 329 | } else { 330 | pbTime[j].acc = Math.floor(parseFloat(pbTime[j].acc)); 331 | } 332 | } 333 | } 334 | 335 | let pbWords = {}; 336 | let words = [10, 25, 50, 100]; 337 | for (let i = 0; i < words.length; i++) { 338 | let english_1k = true; 339 | let english = true; 340 | let english_1k_pb = null; 341 | let english_pb = null; 342 | if (userData.personalBests.words[words[i]] != undefined) { 343 | for ( 344 | let j = 0; 345 | j < userData.personalBests.words[words[i]].length; 346 | j++ 347 | ) { 348 | if ( 349 | userData.personalBests.words[words[i]][j].language == 350 | "english_1k" && 351 | userData.personalBests.words[words[i]][j].difficulty == 352 | "normal" && 353 | userData.personalBests.words[words[i]][j].punctuation == 354 | false && 355 | english_1k == true 356 | ) { 357 | english_1k_pb = 358 | userData.personalBests.words[words[i]][j]; 359 | english_1k = false; 360 | } 361 | if ( 362 | userData.personalBests.words[words[i]][j].language == 363 | "english" && 364 | userData.personalBests.words[words[i]][j].difficulty == 365 | "normal" && 366 | userData.personalBests.words[words[i]][j].punctuation == 367 | false && 368 | english == true 369 | ) { 370 | english_pb = userData.personalBests.words[words[i]][j]; 371 | english = false; 372 | } 373 | } 374 | if (english_1k_pb == null && english_pb == null) { 375 | pbWords[words[i]] = { wpm: "-", acc: "-" }; 376 | } else if (english_1k_pb != null && english_pb == null) { 377 | pbWords[words[i]] = english_1k_pb; 378 | } else if (english_1k_pb == null && english_pb != null) { 379 | pbWords[words[i]] = english_pb; 380 | } else { 381 | if (english_1k_pb.wpm > english_pb.wpm) { 382 | pbWords[words[i]] = english_1k_pb; 383 | } else { 384 | pbWords[words[i]] = english_pb; 385 | } 386 | } 387 | } else { 388 | pbWords[words[i]] = { wpm: "-", acc: "-" }; 389 | } 390 | if (pbWords[words[i]].wpm != "-") { 391 | pbWords[words[i]].wpm = Math.round( 392 | parseFloat(pbWords[words[i]].wpm), 393 | ); 394 | } 395 | if (pbWords[words[i]].acc != "-") { 396 | if ( 397 | pbWords[words[i]].acc == null || 398 | pbWords[words[i]].acc == undefined 399 | ) { 400 | pbWords[words[i]].acc = "-"; 401 | } else { 402 | pbWords[words[i]].acc = Math.floor( 403 | parseFloat(pbWords[words[i]].acc), 404 | ); 405 | } 406 | } 407 | } 408 | 409 | personalbestsHTML = ` 410 |
413 |
414 |
415 |
416 |
417 |
419 | 15 seconds 420 |
421 |
423 | ${pbTime["15"].wpm} 424 |
425 |
427 | ${pbTime["15"].acc}${ 428 | pbTime["15"].acc == "-" ? "" : "%" 429 | } 430 |
431 |
432 |
433 |
435 | 30 seconds 436 |
437 |
439 | ${pbTime["30"].wpm} 440 |
441 |
443 | ${pbTime["30"].acc}${ 444 | pbTime["30"].acc == "-" ? "" : "%" 445 | } 446 |
447 |
448 |
449 |
451 | 60 seconds 452 |
453 |
455 | ${pbTime["60"].wpm} 456 |
457 |
459 | ${pbTime["60"].acc}${ 460 | pbTime["60"].acc == "-" ? "" : "%" 461 | } 462 |
463 |
464 |
465 |
467 | 120 seconds 468 |
469 |
471 | ${pbTime["120"].wpm} 472 |
473 |
475 | ${pbTime["120"].acc}${ 476 | pbTime["120"].acc == "-" ? "" : "%" 477 | } 478 |
479 |
480 |
481 |
482 |
483 |
484 |
487 |
488 |
489 |
490 |
491 |
493 | 10 words 494 |
495 |
497 | ${pbWords["10"].wpm} 498 |
499 |
501 | ${pbWords["10"].acc}${ 502 | pbWords["10"].acc == "-" ? "" : "%" 503 | } 504 |
505 |
506 |
507 |
509 | 25 words 510 |
511 |
513 | ${pbWords["25"].wpm} 514 |
515 |
517 | ${pbWords["25"].acc}${ 518 | pbWords["25"].acc == "-" ? "" : "%" 519 | } 520 |
521 |
522 |
523 |
525 | 50 words 526 |
527 |
529 | ${pbWords["50"].wpm} 530 |
531 |
533 | ${pbWords["50"].acc}${ 534 | pbWords["50"].acc == "-" ? "" : "%" 535 | } 536 |
537 |
538 |
539 |
541 | 100 words 542 |
543 |
545 | ${pbWords["100"].wpm} 546 |
547 |
549 | ${pbWords["100"].acc}${ 550 | pbWords["100"].acc == "-" ? "" : "%" 551 | } 552 |
553 |
554 |
555 |
556 |
557 |
558 | `; 559 | } 560 | 561 | const svg = ` 562 | 564 | 567 | 568 |
569 |
572 |
573 |
${userImg}
574 |
575 |
578 | ${ 579 | userData == null 580 | ? "user not found" 581 | : userData.name 582 | } 583 |
584 | ${ 585 | badge != null 586 | ? `
${userBadge}
` 587 | : `` 588 | } 589 | ${ 590 | userData == null 591 | ? "" 592 | : userData.streak > 0 593 | ? ` 594 |
595 | Current streak: ${userData.streak} days 596 |
597 | ` 598 | : `` 599 | } 600 |
601 |
602 |
603 |
604 | 605 |
${leaderBoardHTML}
606 | 607 |
${personalbestsHTML}
608 | 609 |
610 |
611 |
612 | `; 613 | return svg; 614 | } 615 | 616 | module.exports = { 617 | getOGSvg, 618 | getSvg, 619 | }; 620 | -------------------------------------------------------------------------------- /public/script/index.js: -------------------------------------------------------------------------------- 1 | let monkeytypeName = ""; 2 | let leaderBoardBtnState = false; 3 | let personalBestBtnState = false; 4 | let themeListState = { 5 | themeName: "", 6 | borderColor: "", 7 | }; 8 | 9 | $("#createNowBtn").click(function () { 10 | let targetElement = $("#mr-introduce"); 11 | $("html, body").animate( 12 | { 13 | scrollTop: targetElement.offset().top, 14 | }, 15 | 800, 16 | ); 17 | }); 18 | 19 | $("#monkeytypeNameInput").on("input", function () { 20 | monkeytypeName = $(this).val(); 21 | }); 22 | 23 | $("#leaderBoardBtn").click(function () { 24 | initialReadmeBtn("#leaderBoardBtn", leaderBoardBtnState); 25 | leaderBoardBtnState = !leaderBoardBtnState; 26 | }); 27 | 28 | $("#personalBestBtn").click(function () { 29 | initialReadmeBtn("#personalBestBtn", personalBestBtnState); 30 | personalBestBtnState = !personalBestBtnState; 31 | }); 32 | 33 | $("#generateReadmeBtn").click(async function () { 34 | $("#monkeytypeNameError").addClass("absolute hidden"); 35 | $("#themeNameError").addClass("absolute hidden"); 36 | $("#monkeytypeNameInvalidError").addClass("absolute hidden"); 37 | 38 | let svgDataCheck = false; 39 | const VALID_NAME_PATTERN = /^[\da-zA-Z_.-]+$/; 40 | 41 | if (monkeytypeName === "") { 42 | $("#monkeytypeNameError").removeClass("absolute hidden"); 43 | svgDataCheck = true; 44 | } 45 | 46 | if (themeListState.themeName === "") { 47 | $("#themeNameError").removeClass("absolute hidden"); 48 | svgDataCheck = true; 49 | } 50 | 51 | if ( 52 | monkeytypeName !== "" && 53 | (!VALID_NAME_PATTERN.test(monkeytypeName) || 54 | !(monkeytypeName.length > 1 && monkeytypeName.length < 16)) 55 | ) { 56 | $("#monkeytypeNameInvalidError").removeClass("absolute hidden"); 57 | svgDataCheck = true; 58 | } 59 | 60 | if (svgDataCheck) { 61 | return; 62 | } 63 | 64 | $("#generateReadmeBtn").prop("disabled", true); 65 | $("#generateReadmeBtn").addClass("cursor-not-allowed"); 66 | $("#generateReadmeBtn").removeClass( 67 | "hover:bg-nord-light-green hover:text-nord-light-bg hover:opacity-60", 68 | ); 69 | $("#generateReadmeBtnLoad").removeClass("hidden"); 70 | $("#generateReadmeBtnText").text("Monkeytype Readme Generating..."); 71 | 72 | let themeList = await getMonkeyTypeThemesList(); 73 | let themeData = {}; 74 | 75 | for (let i = 0; i < themeList.length; i++) { 76 | if (themeListState.themeName === themeList[i]["name"]) { 77 | themeData = themeList[i]; 78 | break; 79 | } 80 | } 81 | 82 | let personalReadmeUrl = `${domain}/${monkeytypeName}/${themeListState.themeName}`; 83 | let personalReadmeBtnStyle = `color: ${themeData["mainColor"]}; background-color: ${themeData["bgColor"]}; outline-color: ${themeData["mainColor"]};"`; 84 | 85 | let url = `${domain}/generate-svg/${monkeytypeName}/${themeListState.themeName}`; 86 | if (leaderBoardBtnState && personalBestBtnState) { 87 | url += "?lbpb=true"; 88 | } else { 89 | if (leaderBoardBtnState) { 90 | url += "?lb=true"; 91 | } 92 | if (personalBestBtnState) { 93 | url += "?pb=true"; 94 | } 95 | } 96 | 97 | const img = new Image(); 98 | 99 | img.src = url; 100 | 101 | img.onload = function () { 102 | $("#previewReadmeLink").attr( 103 | "href", 104 | `https://monkeytype.com/profile/${monkeytypeName}`, 105 | ); 106 | $("#previewReadmeImg").attr("src", url); 107 | $("#previewReadmeImg").attr( 108 | "alt", 109 | monkeytypeName + " | Monkeytype Readme", 110 | ); 111 | 112 | $("#personalReadmeLink").attr("href", personalReadmeUrl); 113 | $("#personalReadmeLink").attr( 114 | "title", 115 | `${monkeytypeName} | Monkeytype Readme`, 116 | ); 117 | $("#personalReadmeBtn").attr("style", personalReadmeBtnStyle); 118 | 119 | updateReadmeCode(); 120 | 121 | $("#generateReadmeBtn").prop("disabled", false); 122 | $("#generateReadmeBtn").removeClass("cursor-not-allowed"); 123 | $("#generateReadmeBtn").addClass( 124 | "hover:bg-nord-light-green hover:text-nord-light-bg hover:opacity-60", 125 | ); 126 | 127 | $("#personalReadmeBtn").removeClass("hidden"); 128 | 129 | $("#generateReadmeBtnLoad").addClass("hidden"); 130 | $("#generateReadmeBtnText").text("Generate Monkeytype Readme"); 131 | }; 132 | }); 133 | 134 | $("#monkeytypeNameError").mouseenter(function () { 135 | $("#monkeytypeNameErrorHover").removeClass("hidden"); 136 | }); 137 | 138 | $("#monkeytypeNameError").mouseleave(function () { 139 | $("#monkeytypeNameErrorHover").addClass("hidden"); 140 | }); 141 | 142 | $("#themeNameError").mouseenter(function () { 143 | $("#themeNameErrorHover").removeClass("hidden"); 144 | }); 145 | 146 | $("#themeNameError").mouseleave(function () { 147 | $("#themeNameErrorHover").addClass("hidden"); 148 | }); 149 | 150 | $("#monkeytypeNameInvalidError").mouseenter(function () { 151 | $("#monkeytypeNameInvalidErrorHover").removeClass("hidden"); 152 | }); 153 | 154 | $("#monkeytypeNameInvalidError").mouseleave(function () { 155 | $("#monkeytypeNameInvalidErrorHover").addClass("hidden"); 156 | }); 157 | 158 | function errorHoverClick(id) { 159 | $(`#${id}`).addClass("hidden"); 160 | } 161 | 162 | function initialReadmeBtn(buttonId, buttonState) { 163 | if (!buttonState) { 164 | $(buttonId).removeClass("bg-slate-100 text-gray-400"); 165 | $(buttonId).addClass( 166 | "bg-nord-light-green text-nord-light-bg opacity-60", 167 | ); 168 | } else { 169 | $(buttonId).removeClass( 170 | "bg-nord-light-green text-nord-light-bg opacity-60", 171 | ); 172 | $(buttonId).addClass("bg-slate-100 text-gray-400"); 173 | } 174 | } 175 | 176 | function showThemeList() { 177 | $("#showThemeBtn").addClass("hidden"); 178 | $("#themeListContainer").removeClass("h-96"); 179 | $("#hideThemeBtn").removeClass("hidden"); 180 | } 181 | 182 | function hideThemeList() { 183 | let targetElement = $("#mr-create"); 184 | $("html, body").animate( 185 | { 186 | scrollTop: targetElement.offset().top, 187 | }, 188 | 800, 189 | ); 190 | setTimeout(() => { 191 | $("#showThemeBtn").removeClass("hidden"); 192 | $("#themeListContainer").addClass("h-96"); 193 | $("#hideThemeBtn").addClass("hidden"); 194 | }, 800); 195 | } 196 | 197 | function showBorder(themeName) { 198 | const borderColor = $(`#${themeName}`).css("border-color"); 199 | if (themeName === themeListState.themeName) { 200 | $(`#${themeName}`).css("border", ""); 201 | $(`#${themeName}`).css("border-color", themeListState.borderColor); 202 | themeListState.themeName = ""; 203 | } else { 204 | if (themeListState.themeName !== "") { 205 | $(`#${themeListState.themeName}`).css("border", ""); 206 | $(`#${themeListState.themeName}`).css( 207 | "border-color", 208 | themeListState.borderColor, 209 | ); 210 | } 211 | $(`#${themeName}`).css("border", `4px solid ${borderColor}`); 212 | themeListState.themeName = themeName; 213 | themeListState.borderColor = borderColor; 214 | } 215 | } 216 | 217 | function updateReadmeCode() { 218 | const githubReamdeYml = ` 219 |
                        
220 |     name: generate monkeytype readme svg
221 |     
222 |     on:
223 |     schedule:
224 |         - cron: "0 */6 * * *" # every 6 hours
225 |     workflow_dispatch:
226 |     
227 |     jobs:
228 |     download-svg:
229 |         runs-on: ubuntu-latest
230 |         steps:
231 |         - name: Checkout code
232 |             uses: actions/checkout@v3
233 |     
234 |         - name: Set up Node.js
235 |             uses: actions/setup-node@v3
236 |             with:
237 |             node-version: '16.x'
238 |     
239 |         - name: Download SVG
240 |             run: |
241 |             mkdir public
242 |             curl -o public/monkeytype-readme.svg ${domain}/generate-svg/${monkeytypeName}/${themeListState.themeName}
243 |             curl -o public/monkeytype-readme-lb.svg ${domain}/generate-svg/${monkeytypeName}/${themeListState.themeName}?lb=true
244 |             curl -o public/monkeytype-readme-pb.svg ${domain}/generate-svg/${monkeytypeName}/${themeListState.themeName}?pb=true
245 |             curl -o public/monkeytype-readme-lb-pb.svg ${domain}/generate-svg/${monkeytypeName}/${themeListState.themeName}?lbpb=true
246 |     
247 |         - name: push monkeytype-readme.svg to the monkeytype-readme branch
248 |             uses: crazy-max/ghaction-github-pages@v2.5.0
249 |             with:
250 |             target_branch: monkeytype-readme
251 |             build_dir: public
252 |             env:
253 |             GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
254 |                 
255 |                     
256 | `; 257 | $("#githubReadmeYml").empty(); 258 | $("#githubReadmeYml").append(githubReamdeYml); 259 | 260 | const githubReamdeMd = ` 261 |
                        
262 |     <a href="https://monkeytype.com/profile/${monkeytypeName}">
263 |         <img src="https://raw.githubusercontent.com/GITHUB_USERNAME/GITHUB_REPOSITORY/monkeytype-readme/monkeytype-readme-lb.svg" alt="My Monkeytype profile" />
264 |     </a>
265 |                         
266 |                     
267 | `; 268 | $("#githubReadmeMd").empty(); 269 | $("#githubReadmeMd").append(githubReamdeMd); 270 | } 271 | 272 | function hexToRgb(hex) { 273 | // Remove the # symbol if present 274 | hex = hex.replace("#", ""); 275 | 276 | // Check if the hex code is three characters long 277 | if (hex.length === 3) { 278 | // Duplicate each character to expand the code to six characters 279 | hex = hex.replace(/(.)/g, "$1$1"); 280 | } 281 | 282 | // Convert the hex value to RGB 283 | const r = parseInt(hex.substring(0, 2), 16); 284 | const g = parseInt(hex.substring(2, 4), 16); 285 | const b = parseInt(hex.substring(4, 6), 16); 286 | 287 | return { r, g, b }; 288 | } 289 | 290 | function compareColors(hex1, hex2, themeName1, themeName2) { 291 | const rgb1 = hexToRgb(hex1); 292 | const rgb2 = hexToRgb(hex2); 293 | const brightness1 = rgb1.r + rgb1.g + rgb1.b; 294 | const brightness2 = rgb2.r + rgb2.g + rgb2.b; 295 | 296 | if (brightness1 > brightness2) { 297 | return true; 298 | } else if (brightness1 == brightness2) { 299 | if (themeName1 < themeName2) { 300 | return true; 301 | } else { 302 | return false; 303 | } 304 | } else { 305 | return false; 306 | } 307 | } 308 | 309 | async function getMonkeyTypeThemesList() { 310 | const url = 311 | "https://raw.githubusercontent.com/monkeytype-hub/monkeytype-readme/refs/heads/master/monkeytype-data/themes.json"; 312 | 313 | return fetch(url) 314 | .then((response) => response.json()) 315 | .then((data) => { 316 | return data; 317 | }); 318 | } 319 | 320 | async function themeList() { 321 | let themeList = await getMonkeyTypeThemesList(); 322 | 323 | for (let i = 0; i < themeList.length; i++) { 324 | for (let j = i + 1; j < themeList.length; j++) { 325 | if ( 326 | compareColors( 327 | themeList[i]["bgColor"], 328 | themeList[j]["bgColor"], 329 | themeList[i]["name"], 330 | themeList[j]["name"], 331 | ) 332 | ) { 333 | let temp = themeList[i]; 334 | themeList[i] = themeList[j]; 335 | themeList[j] = temp; 336 | } 337 | } 338 | } 339 | 340 | for (let i = themeList.length - 1; i >= 0; i--) { 341 | let html = ` 342 | 354 | `; 355 | $("#themeListContainer").append(html); 356 | } 357 | } 358 | 359 | themeList(); 360 | -------------------------------------------------------------------------------- /public/script/monkeytypeData.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | let fetch; 3 | import("node-fetch") 4 | .then((module) => { 5 | fetch = module.default; 6 | }) 7 | .catch((err) => { 8 | console.error("Error while importing node-fetch:", err); 9 | }); 10 | 11 | const { library } = require("@fortawesome/fontawesome-svg-core"); 12 | const { fas } = require("@fortawesome/free-solid-svg-icons"); 13 | const { far } = require("@fortawesome/free-regular-svg-icons"); 14 | const { fab } = require("@fortawesome/free-brands-svg-icons"); 15 | const { findIconDefinition } = require("@fortawesome/fontawesome-svg-core"); 16 | 17 | library.add(fas, far, fab); 18 | 19 | function getTheme(themeName) { 20 | const themesRawData = fs.readFileSync("./monkeytype-data/themes.json"); 21 | const themesData = JSON.parse(themesRawData); 22 | let serika_dark = {}; 23 | 24 | for (let i = 0; i < themesData.length; i++) { 25 | const theme = themesData[i]; 26 | if (theme.name === themeName) { 27 | return theme; 28 | } 29 | if (theme.name === "serika_dark") { 30 | serika_dark = theme; 31 | } 32 | } 33 | 34 | return serika_dark; 35 | } 36 | 37 | function getFaviconTheme() { 38 | const themesRawData = fs.readFileSync("./monkeytype-data/themes.json"); 39 | const themesData = JSON.parse(themesRawData); 40 | let faviconData = {}; 41 | 42 | let data; 43 | for (let i = 0; i < themesData.length; i++) { 44 | data = { 45 | name: themesData[i].name, 46 | bgColor: themesData[i].bgColor, 47 | mainColor: themesData[i].mainColor, 48 | subColor: themesData[i].subColor, 49 | textColor: themesData[i].textColor, 50 | }; 51 | faviconData[i] = data; 52 | } 53 | 54 | return faviconData; 55 | } 56 | 57 | function getBadge(badgeId) { 58 | const badgesRawData = fs.readFileSync("./monkeytype-data/badges.json"); 59 | const badgesData = JSON.parse(badgesRawData); 60 | return badgesData[badgeId]; 61 | } 62 | 63 | async function getUserData(userId) { 64 | const url = `https://api.monkeytype.com/users/${userId}/profile`; 65 | 66 | try { 67 | return fetch(url) 68 | .then((response) => response.json()) 69 | .then((data) => { 70 | return data.data; 71 | }); 72 | } catch (error) { 73 | console.error(error); 74 | } 75 | } 76 | 77 | async function getMonkeyTypeThemesData() { 78 | const url = "https://mr-api.zeabur.app/themes"; 79 | 80 | try { 81 | return fetch(url) 82 | .then((response) => response.json()) 83 | .then((data) => { 84 | return data; 85 | }); 86 | } catch (error) { 87 | console.error(error); 88 | } 89 | } 90 | 91 | async function getMonkeyTypeBadgesData() { 92 | const url = 93 | "https://raw.githubusercontent.com/monkeytypegame/monkeytype/master/frontend/src/ts/controllers/badge-controller.ts"; 94 | 95 | return fetch(url) 96 | .then((response) => response.text()) 97 | .then((data) => { 98 | const badgesStart = data.search( 99 | "const badges: Record = {", 100 | ); 101 | const badgesDataStart = 102 | badgesStart + 103 | "const badges: Record = ".length; 104 | const badgesDataEnd = data.indexOf("};", badgesDataStart); 105 | let badgesData = data.substring(badgesDataStart, badgesDataEnd + 1); 106 | badgesData = badgesData 107 | .replace(/(\w+)\s*:/g, '"$1":') 108 | .replace(/,(\s*[\]}])/g, "$1") 109 | .replace( 110 | /(\w+:)|(\w+ :)/g, 111 | (matchedStr) => '"' + matchedStr.replace(/:/g, "") + '":', 112 | ) 113 | .replace(/\"/g, '"') 114 | .replace( 115 | /"customStyle"\s*:\s*"([^"]*\"animation\"[^"]*\"background\"[^"]*)"/g, 116 | (match, customStyleValue) => { 117 | const updatedCustomStyle = customStyleValue 118 | .replace(/\"animation\"/g, "animation") 119 | .replace(/\"background\"/g, "background"); 120 | return `"customStyle": "${updatedCustomStyle}"`; 121 | }, 122 | ); 123 | badgesData = JSON.parse(badgesData); 124 | 125 | for (let i = 0; i < Object.keys(badgesData).length; i++) { 126 | let badge = badgesData[Object.keys(badgesData)[i]]; 127 | if (badge.color.includes("var")) { 128 | badge.color = badge.color 129 | .replace("var(--", "") 130 | .replace(")", "") 131 | .replace("-", "") 132 | .replace("c", "C"); 133 | } 134 | if (badge.background && badge.background.includes("var")) { 135 | badge.background = badge.background 136 | .replace("var(--", "") 137 | .replace(")", "") 138 | .replace("-", "") 139 | .replace("c", "C"); 140 | } 141 | const iconSvg = findIconDefinition({ 142 | prefix: "fas", 143 | iconName: `${badge.icon.replace("fa-", "")}`, 144 | }); 145 | badge[ 146 | "iconSvg" 147 | ] = ``; 148 | } 149 | 150 | badgesData = JSON.stringify(badgesData, null, 4); 151 | 152 | return badgesData; 153 | }) 154 | .catch((error) => console.error(error)); 155 | } 156 | 157 | module.exports = { 158 | getTheme, 159 | getFaviconTheme, 160 | getBadge, 161 | getUserData, 162 | getMonkeyTypeThemesData, 163 | getMonkeyTypeBadgesData, 164 | }; 165 | -------------------------------------------------------------------------------- /public/script/tailwindCSS.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const fs = require("fs"); 3 | const readFile = util.promisify(fs.readFile); 4 | 5 | async function getOutputCSS() { 6 | try { 7 | const data = await readFile("public/style/output.css", "utf-8"); 8 | return data; 9 | } catch (err) { 10 | console.error(err); 11 | return; 12 | } 13 | } 14 | 15 | module.exports = { 16 | getOutputCSS, 17 | }; 18 | -------------------------------------------------------------------------------- /public/script/user.js: -------------------------------------------------------------------------------- 1 | let shareListState = false; 2 | 3 | $("#shareBtn").click(function () { 4 | if (!shareListState) { 5 | $("#shareList").removeClass("hidden"); 6 | shareListState = true; 7 | } else { 8 | $("#shareList").addClass("hidden"); 9 | shareListState = false; 10 | } 11 | }); 12 | 13 | $(document).click(function (event) { 14 | if ( 15 | !$(event.target).closest( 16 | "#shareTwitterBtn, #shareFacebookBtn, #shareUrlBtn, #shareBtn", 17 | ).length && 18 | shareListState == true 19 | ) { 20 | $("#shareList").addClass("hidden"); 21 | shareListState = false; 22 | } 23 | }); 24 | 25 | async function copyReadmeUrl(copyUrl) { 26 | await navigator.clipboard.writeText(copyUrl); 27 | } 28 | -------------------------------------------------------------------------------- /public/style/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/style/output.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | */ 36 | 37 | html { 38 | line-height: 1.5; 39 | /* 1 */ 40 | -webkit-text-size-adjust: 100%; 41 | /* 2 */ 42 | -moz-tab-size: 4; 43 | /* 3 */ 44 | -o-tab-size: 4; 45 | tab-size: 4; 46 | /* 3 */ 47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 48 | /* 4 */ 49 | font-feature-settings: normal; 50 | /* 5 */ 51 | font-variation-settings: normal; 52 | /* 6 */ 53 | } 54 | 55 | /* 56 | 1. Remove the margin in all browsers. 57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 58 | */ 59 | 60 | body { 61 | margin: 0; 62 | /* 1 */ 63 | line-height: inherit; 64 | /* 2 */ 65 | } 66 | 67 | /* 68 | 1. Add the correct height in Firefox. 69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 70 | 3. Ensure horizontal rules are visible by default. 71 | */ 72 | 73 | hr { 74 | height: 0; 75 | /* 1 */ 76 | color: inherit; 77 | /* 2 */ 78 | border-top-width: 1px; 79 | /* 3 */ 80 | } 81 | 82 | /* 83 | Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr:where([title]) { 87 | -webkit-text-decoration: underline dotted; 88 | text-decoration: underline dotted; 89 | } 90 | 91 | /* 92 | Remove the default font size and weight for headings. 93 | */ 94 | 95 | h1, 96 | h2, 97 | h3, 98 | h4, 99 | h5, 100 | h6 { 101 | font-size: inherit; 102 | font-weight: inherit; 103 | } 104 | 105 | /* 106 | Reset links to optimize for opt-in styling instead of opt-out. 107 | */ 108 | 109 | a { 110 | color: inherit; 111 | text-decoration: inherit; 112 | } 113 | 114 | /* 115 | Add the correct font weight in Edge and Safari. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bolder; 121 | } 122 | 123 | /* 124 | 1. Use the user's configured `mono` font family by default. 125 | 2. Correct the odd `em` font sizing in all browsers. 126 | */ 127 | 128 | code, 129 | kbd, 130 | samp, 131 | pre { 132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 133 | /* 1 */ 134 | font-size: 1em; 135 | /* 2 */ 136 | } 137 | 138 | /* 139 | Add the correct font size in all browsers. 140 | */ 141 | 142 | small { 143 | font-size: 80%; 144 | } 145 | 146 | /* 147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 148 | */ 149 | 150 | sub, 151 | sup { 152 | font-size: 75%; 153 | line-height: 0; 154 | position: relative; 155 | vertical-align: baseline; 156 | } 157 | 158 | sub { 159 | bottom: -0.25em; 160 | } 161 | 162 | sup { 163 | top: -0.5em; 164 | } 165 | 166 | /* 167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 169 | 3. Remove gaps between table borders by default. 170 | */ 171 | 172 | table { 173 | text-indent: 0; 174 | /* 1 */ 175 | border-color: inherit; 176 | /* 2 */ 177 | border-collapse: collapse; 178 | /* 3 */ 179 | } 180 | 181 | /* 182 | 1. Change the font styles in all browsers. 183 | 2. Remove the margin in Firefox and Safari. 184 | 3. Remove default padding in all browsers. 185 | */ 186 | 187 | button, 188 | input, 189 | optgroup, 190 | select, 191 | textarea { 192 | font-family: inherit; 193 | /* 1 */ 194 | font-feature-settings: inherit; 195 | /* 1 */ 196 | font-variation-settings: inherit; 197 | /* 1 */ 198 | font-size: 100%; 199 | /* 1 */ 200 | font-weight: inherit; 201 | /* 1 */ 202 | line-height: inherit; 203 | /* 1 */ 204 | color: inherit; 205 | /* 1 */ 206 | margin: 0; 207 | /* 2 */ 208 | padding: 0; 209 | /* 3 */ 210 | } 211 | 212 | /* 213 | Remove the inheritance of text transform in Edge and Firefox. 214 | */ 215 | 216 | button, 217 | select { 218 | text-transform: none; 219 | } 220 | 221 | /* 222 | 1. Correct the inability to style clickable types in iOS and Safari. 223 | 2. Remove default button styles. 224 | */ 225 | 226 | button, 227 | [type='button'], 228 | [type='reset'], 229 | [type='submit'] { 230 | -webkit-appearance: button; 231 | /* 1 */ 232 | background-color: transparent; 233 | /* 2 */ 234 | background-image: none; 235 | /* 2 */ 236 | } 237 | 238 | /* 239 | Use the modern Firefox focus style for all focusable elements. 240 | */ 241 | 242 | :-moz-focusring { 243 | outline: auto; 244 | } 245 | 246 | /* 247 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 248 | */ 249 | 250 | :-moz-ui-invalid { 251 | box-shadow: none; 252 | } 253 | 254 | /* 255 | Add the correct vertical alignment in Chrome and Firefox. 256 | */ 257 | 258 | progress { 259 | vertical-align: baseline; 260 | } 261 | 262 | /* 263 | Correct the cursor style of increment and decrement buttons in Safari. 264 | */ 265 | 266 | ::-webkit-inner-spin-button, 267 | ::-webkit-outer-spin-button { 268 | height: auto; 269 | } 270 | 271 | /* 272 | 1. Correct the odd appearance in Chrome and Safari. 273 | 2. Correct the outline style in Safari. 274 | */ 275 | 276 | [type='search'] { 277 | -webkit-appearance: textfield; 278 | /* 1 */ 279 | outline-offset: -2px; 280 | /* 2 */ 281 | } 282 | 283 | /* 284 | Remove the inner padding in Chrome and Safari on macOS. 285 | */ 286 | 287 | ::-webkit-search-decoration { 288 | -webkit-appearance: none; 289 | } 290 | 291 | /* 292 | 1. Correct the inability to style clickable types in iOS and Safari. 293 | 2. Change font properties to `inherit` in Safari. 294 | */ 295 | 296 | ::-webkit-file-upload-button { 297 | -webkit-appearance: button; 298 | /* 1 */ 299 | font: inherit; 300 | /* 2 */ 301 | } 302 | 303 | /* 304 | Add the correct display in Chrome and Safari. 305 | */ 306 | 307 | summary { 308 | display: list-item; 309 | } 310 | 311 | /* 312 | Removes the default spacing and border for appropriate elements. 313 | */ 314 | 315 | blockquote, 316 | dl, 317 | dd, 318 | h1, 319 | h2, 320 | h3, 321 | h4, 322 | h5, 323 | h6, 324 | hr, 325 | figure, 326 | p, 327 | pre { 328 | margin: 0; 329 | } 330 | 331 | fieldset { 332 | margin: 0; 333 | padding: 0; 334 | } 335 | 336 | legend { 337 | padding: 0; 338 | } 339 | 340 | ol, 341 | ul, 342 | menu { 343 | list-style: none; 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | /* 349 | Reset default styling for dialogs. 350 | */ 351 | 352 | dialog { 353 | padding: 0; 354 | } 355 | 356 | /* 357 | Prevent resizing textareas horizontally by default. 358 | */ 359 | 360 | textarea { 361 | resize: vertical; 362 | } 363 | 364 | /* 365 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 366 | 2. Set the default placeholder color to the user's configured gray 400 color. 367 | */ 368 | 369 | input::-moz-placeholder, textarea::-moz-placeholder { 370 | opacity: 1; 371 | /* 1 */ 372 | color: #9ca3af; 373 | /* 2 */ 374 | } 375 | 376 | input::placeholder, 377 | textarea::placeholder { 378 | opacity: 1; 379 | /* 1 */ 380 | color: #9ca3af; 381 | /* 2 */ 382 | } 383 | 384 | /* 385 | Set the default cursor for buttons. 386 | */ 387 | 388 | button, 389 | [role="button"] { 390 | cursor: pointer; 391 | } 392 | 393 | /* 394 | Make sure disabled buttons don't get the pointer cursor. 395 | */ 396 | 397 | :disabled { 398 | cursor: default; 399 | } 400 | 401 | /* 402 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 403 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 404 | This can trigger a poorly considered lint error in some tools but is included by design. 405 | */ 406 | 407 | img, 408 | svg, 409 | video, 410 | canvas, 411 | audio, 412 | iframe, 413 | embed, 414 | object { 415 | display: block; 416 | /* 1 */ 417 | vertical-align: middle; 418 | /* 2 */ 419 | } 420 | 421 | /* 422 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 423 | */ 424 | 425 | img, 426 | video { 427 | max-width: 100%; 428 | height: auto; 429 | } 430 | 431 | /* Make elements with the HTML hidden attribute stay hidden by default */ 432 | 433 | [hidden] { 434 | display: none; 435 | } 436 | 437 | *, ::before, ::after { 438 | --tw-border-spacing-x: 0; 439 | --tw-border-spacing-y: 0; 440 | --tw-translate-x: 0; 441 | --tw-translate-y: 0; 442 | --tw-rotate: 0; 443 | --tw-skew-x: 0; 444 | --tw-skew-y: 0; 445 | --tw-scale-x: 1; 446 | --tw-scale-y: 1; 447 | --tw-pan-x: ; 448 | --tw-pan-y: ; 449 | --tw-pinch-zoom: ; 450 | --tw-scroll-snap-strictness: proximity; 451 | --tw-gradient-from-position: ; 452 | --tw-gradient-via-position: ; 453 | --tw-gradient-to-position: ; 454 | --tw-ordinal: ; 455 | --tw-slashed-zero: ; 456 | --tw-numeric-figure: ; 457 | --tw-numeric-spacing: ; 458 | --tw-numeric-fraction: ; 459 | --tw-ring-inset: ; 460 | --tw-ring-offset-width: 0px; 461 | --tw-ring-offset-color: #fff; 462 | --tw-ring-color: rgb(59 130 246 / 0.5); 463 | --tw-ring-offset-shadow: 0 0 #0000; 464 | --tw-ring-shadow: 0 0 #0000; 465 | --tw-shadow: 0 0 #0000; 466 | --tw-shadow-colored: 0 0 #0000; 467 | --tw-blur: ; 468 | --tw-brightness: ; 469 | --tw-contrast: ; 470 | --tw-grayscale: ; 471 | --tw-hue-rotate: ; 472 | --tw-invert: ; 473 | --tw-saturate: ; 474 | --tw-sepia: ; 475 | --tw-drop-shadow: ; 476 | --tw-backdrop-blur: ; 477 | --tw-backdrop-brightness: ; 478 | --tw-backdrop-contrast: ; 479 | --tw-backdrop-grayscale: ; 480 | --tw-backdrop-hue-rotate: ; 481 | --tw-backdrop-invert: ; 482 | --tw-backdrop-opacity: ; 483 | --tw-backdrop-saturate: ; 484 | --tw-backdrop-sepia: ; 485 | } 486 | 487 | ::backdrop { 488 | --tw-border-spacing-x: 0; 489 | --tw-border-spacing-y: 0; 490 | --tw-translate-x: 0; 491 | --tw-translate-y: 0; 492 | --tw-rotate: 0; 493 | --tw-skew-x: 0; 494 | --tw-skew-y: 0; 495 | --tw-scale-x: 1; 496 | --tw-scale-y: 1; 497 | --tw-pan-x: ; 498 | --tw-pan-y: ; 499 | --tw-pinch-zoom: ; 500 | --tw-scroll-snap-strictness: proximity; 501 | --tw-gradient-from-position: ; 502 | --tw-gradient-via-position: ; 503 | --tw-gradient-to-position: ; 504 | --tw-ordinal: ; 505 | --tw-slashed-zero: ; 506 | --tw-numeric-figure: ; 507 | --tw-numeric-spacing: ; 508 | --tw-numeric-fraction: ; 509 | --tw-ring-inset: ; 510 | --tw-ring-offset-width: 0px; 511 | --tw-ring-offset-color: #fff; 512 | --tw-ring-color: rgb(59 130 246 / 0.5); 513 | --tw-ring-offset-shadow: 0 0 #0000; 514 | --tw-ring-shadow: 0 0 #0000; 515 | --tw-shadow: 0 0 #0000; 516 | --tw-shadow-colored: 0 0 #0000; 517 | --tw-blur: ; 518 | --tw-brightness: ; 519 | --tw-contrast: ; 520 | --tw-grayscale: ; 521 | --tw-hue-rotate: ; 522 | --tw-invert: ; 523 | --tw-saturate: ; 524 | --tw-sepia: ; 525 | --tw-drop-shadow: ; 526 | --tw-backdrop-blur: ; 527 | --tw-backdrop-brightness: ; 528 | --tw-backdrop-contrast: ; 529 | --tw-backdrop-grayscale: ; 530 | --tw-backdrop-hue-rotate: ; 531 | --tw-backdrop-invert: ; 532 | --tw-backdrop-opacity: ; 533 | --tw-backdrop-saturate: ; 534 | --tw-backdrop-sepia: ; 535 | } 536 | 537 | .pointer-events-none { 538 | pointer-events: none; 539 | } 540 | 541 | .static { 542 | position: static; 543 | } 544 | 545 | .absolute { 546 | position: absolute; 547 | } 548 | 549 | .relative { 550 | position: relative; 551 | } 552 | 553 | .-top-16 { 554 | top: -4rem; 555 | } 556 | 557 | .bottom-0 { 558 | bottom: 0px; 559 | } 560 | 561 | .left-0 { 562 | left: 0px; 563 | } 564 | 565 | .right-0 { 566 | right: 0px; 567 | } 568 | 569 | .top-0 { 570 | top: 0px; 571 | } 572 | 573 | .isolate { 574 | isolation: isolate; 575 | } 576 | 577 | .z-10 { 578 | z-index: 10; 579 | } 580 | 581 | .col-span-1 { 582 | grid-column: span 1 / span 1; 583 | } 584 | 585 | .col-span-5 { 586 | grid-column: span 5 / span 5; 587 | } 588 | 589 | .mx-1 { 590 | margin-left: 0.25rem; 591 | margin-right: 0.25rem; 592 | } 593 | 594 | .mx-2 { 595 | margin-left: 0.5rem; 596 | margin-right: 0.5rem; 597 | } 598 | 599 | .mx-5 { 600 | margin-left: 1.25rem; 601 | margin-right: 1.25rem; 602 | } 603 | 604 | .my-16 { 605 | margin-top: 4rem; 606 | margin-bottom: 4rem; 607 | } 608 | 609 | .-mt-3 { 610 | margin-top: -0.75rem; 611 | } 612 | 613 | .mb-10 { 614 | margin-bottom: 2.5rem; 615 | } 616 | 617 | .mb-3 { 618 | margin-bottom: 0.75rem; 619 | } 620 | 621 | .ml-2 { 622 | margin-left: 0.5rem; 623 | } 624 | 625 | .ml-3 { 626 | margin-left: 0.75rem; 627 | } 628 | 629 | .ml-4 { 630 | margin-left: 1rem; 631 | } 632 | 633 | .ml-6 { 634 | margin-left: 1.5rem; 635 | } 636 | 637 | .mr-2 { 638 | margin-right: 0.5rem; 639 | } 640 | 641 | .mt-0 { 642 | margin-top: 0px; 643 | } 644 | 645 | .mt-0\.5 { 646 | margin-top: 0.125rem; 647 | } 648 | 649 | .mt-1 { 650 | margin-top: 0.25rem; 651 | } 652 | 653 | .mt-16 { 654 | margin-top: 4rem; 655 | } 656 | 657 | .mt-2 { 658 | margin-top: 0.5rem; 659 | } 660 | 661 | .mt-3 { 662 | margin-top: 0.75rem; 663 | } 664 | 665 | .mt-4 { 666 | margin-top: 1rem; 667 | } 668 | 669 | .mt-44 { 670 | margin-top: 11rem; 671 | } 672 | 673 | .mt-5 { 674 | margin-top: 1.25rem; 675 | } 676 | 677 | .mt-6 { 678 | margin-top: 1.5rem; 679 | } 680 | 681 | .mt-8 { 682 | margin-top: 2rem; 683 | } 684 | 685 | .block { 686 | display: block; 687 | } 688 | 689 | .flex { 690 | display: flex; 691 | } 692 | 693 | .grid { 694 | display: grid; 695 | } 696 | 697 | .hidden { 698 | display: none; 699 | } 700 | 701 | .h-1\/6 { 702 | height: 16.666667%; 703 | } 704 | 705 | .h-10 { 706 | height: 2.5rem; 707 | } 708 | 709 | .h-12 { 710 | height: 3rem; 711 | } 712 | 713 | .h-14 { 714 | height: 3.5rem; 715 | } 716 | 717 | .h-16 { 718 | height: 4rem; 719 | } 720 | 721 | .h-20 { 722 | height: 5rem; 723 | } 724 | 725 | .h-5\/6 { 726 | height: 83.333333%; 727 | } 728 | 729 | .h-60 { 730 | height: 15rem; 731 | } 732 | 733 | .h-8 { 734 | height: 2rem; 735 | } 736 | 737 | .h-96 { 738 | height: 24rem; 739 | } 740 | 741 | .h-\[85\%\] { 742 | height: 85%; 743 | } 744 | 745 | .h-full { 746 | height: 100%; 747 | } 748 | 749 | .h-screen { 750 | height: 100vh; 751 | } 752 | 753 | .w-10 { 754 | width: 2.5rem; 755 | } 756 | 757 | .w-20 { 758 | width: 5rem; 759 | } 760 | 761 | .w-26 { 762 | width: 6.5rem; 763 | } 764 | 765 | .w-28 { 766 | width: 7rem; 767 | } 768 | 769 | .w-32 { 770 | width: 8rem; 771 | } 772 | 773 | .w-33 { 774 | width: 8.25rem; 775 | } 776 | 777 | .w-8 { 778 | width: 2rem; 779 | } 780 | 781 | .w-fit { 782 | width: -moz-fit-content; 783 | width: fit-content; 784 | } 785 | 786 | .w-full { 787 | width: 100%; 788 | } 789 | 790 | @keyframes bounce { 791 | 0%, 100% { 792 | transform: translateY(-25%); 793 | animation-timing-function: cubic-bezier(0.8,0,1,1); 794 | } 795 | 796 | 50% { 797 | transform: none; 798 | animation-timing-function: cubic-bezier(0,0,0.2,1); 799 | } 800 | } 801 | 802 | .animate-bounce { 803 | animation: bounce 1s infinite; 804 | } 805 | 806 | @keyframes fade-left { 807 | 0% { 808 | opacity: 0; 809 | transform: translateX(2rem); 810 | } 811 | 812 | 100% { 813 | opacity: 1; 814 | transform: translateX(0); 815 | } 816 | } 817 | 818 | .animate-fade-left { 819 | animation: fade-left 1s both; 820 | } 821 | 822 | @keyframes fade-right { 823 | 0% { 824 | opacity: 0; 825 | transform: translateX(-2rem); 826 | } 827 | 828 | 100% { 829 | opacity: 1; 830 | transform: translateX(0); 831 | } 832 | } 833 | 834 | .animate-fade-right { 835 | animation: fade-right 1s both; 836 | } 837 | 838 | @keyframes fade-up { 839 | 0% { 840 | opacity: 0; 841 | transform: translateY(2rem); 842 | } 843 | 844 | 100% { 845 | opacity: 1; 846 | transform: translateY(0); 847 | } 848 | } 849 | 850 | .animate-fade-up { 851 | animation: fade-up 1s both; 852 | } 853 | 854 | @keyframes rgb-bg { 855 | 0% { 856 | background-color: hsl(120, 39%, 49%); 857 | } 858 | 859 | 20% { 860 | background-color: hsl(192, 48%, 48%); 861 | } 862 | 863 | 40% { 864 | background-color: hsl(264, 90%, 58%); 865 | } 866 | 867 | 60% { 868 | background-color: hsl(357, 89%, 50%); 869 | } 870 | 871 | 80% { 872 | background-color: hsl(46, 100%, 51%); 873 | } 874 | 875 | 100% { 876 | background-color: hsl(120, 39%, 49%); 877 | } 878 | } 879 | 880 | .animate-rgb-bg { 881 | animation: rgb-bg 10s linear infinite; 882 | } 883 | 884 | @keyframes spin { 885 | to { 886 | transform: rotate(360deg); 887 | } 888 | } 889 | 890 | .animate-spin { 891 | animation: spin 1s linear infinite; 892 | } 893 | 894 | .cursor-not-allowed { 895 | cursor: not-allowed; 896 | } 897 | 898 | .grid-cols-1 { 899 | grid-template-columns: repeat(1, minmax(0, 1fr)); 900 | } 901 | 902 | .grid-cols-3 { 903 | grid-template-columns: repeat(3, minmax(0, 1fr)); 904 | } 905 | 906 | .flex-col { 907 | flex-direction: column; 908 | } 909 | 910 | .items-end { 911 | align-items: flex-end; 912 | } 913 | 914 | .items-center { 915 | align-items: center; 916 | } 917 | 918 | .justify-start { 919 | justify-content: flex-start; 920 | } 921 | 922 | .justify-center { 923 | justify-content: center; 924 | } 925 | 926 | .justify-between { 927 | justify-content: space-between; 928 | } 929 | 930 | .justify-around { 931 | justify-content: space-around; 932 | } 933 | 934 | .gap-2 { 935 | gap: 0.5rem; 936 | } 937 | 938 | .gap-4 { 939 | gap: 1rem; 940 | } 941 | 942 | .gap-x-5 { 943 | -moz-column-gap: 1.25rem; 944 | column-gap: 1.25rem; 945 | } 946 | 947 | .overflow-hidden { 948 | overflow: hidden; 949 | } 950 | 951 | .rounded-2xl { 952 | border-radius: 1rem; 953 | } 954 | 955 | .rounded-full { 956 | border-radius: 9999px; 957 | } 958 | 959 | .rounded-lg { 960 | border-radius: 0.5rem; 961 | } 962 | 963 | .rounded-md { 964 | border-radius: 0.375rem; 965 | } 966 | 967 | .rounded-xl { 968 | border-radius: 0.75rem; 969 | } 970 | 971 | .border { 972 | border-width: 1px; 973 | } 974 | 975 | .border-4 { 976 | border-width: 4px; 977 | } 978 | 979 | .border-nord-light-green { 980 | --tw-border-opacity: 1; 981 | border-color: rgb(143 188 187 / var(--tw-border-opacity)); 982 | } 983 | 984 | .border-nord-light-subAlt { 985 | --tw-border-opacity: 1; 986 | border-color: rgb(216 222 233 / var(--tw-border-opacity)); 987 | } 988 | 989 | .border-transparent { 990 | border-color: transparent; 991 | } 992 | 993 | .bg-black { 994 | --tw-bg-opacity: 1; 995 | background-color: rgb(0 0 0 / var(--tw-bg-opacity)); 996 | } 997 | 998 | .bg-code-bg-brown { 999 | --tw-bg-opacity: 1; 1000 | background-color: rgb(245 242 240 / var(--tw-bg-opacity)); 1001 | } 1002 | 1003 | .bg-gray-900 { 1004 | --tw-bg-opacity: 1; 1005 | background-color: rgb(17 24 39 / var(--tw-bg-opacity)); 1006 | } 1007 | 1008 | .bg-nord-light-error { 1009 | --tw-bg-opacity: 1; 1010 | background-color: rgb(191 97 106 / var(--tw-bg-opacity)); 1011 | } 1012 | 1013 | .bg-nord-light-green { 1014 | --tw-bg-opacity: 1; 1015 | background-color: rgb(143 188 187 / var(--tw-bg-opacity)); 1016 | } 1017 | 1018 | .bg-red-50 { 1019 | --tw-bg-opacity: 1; 1020 | background-color: rgb(254 242 242 / var(--tw-bg-opacity)); 1021 | } 1022 | 1023 | .bg-slate-100 { 1024 | --tw-bg-opacity: 1; 1025 | background-color: rgb(241 245 249 / var(--tw-bg-opacity)); 1026 | } 1027 | 1028 | .bg-white { 1029 | --tw-bg-opacity: 1; 1030 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 1031 | } 1032 | 1033 | .bg-opacity-20 { 1034 | --tw-bg-opacity: 0.2; 1035 | } 1036 | 1037 | .bg-opacity-40 { 1038 | --tw-bg-opacity: 0.4; 1039 | } 1040 | 1041 | .bg-gradient-to-br { 1042 | background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); 1043 | } 1044 | 1045 | .from-nord-light-subAlt { 1046 | --tw-gradient-from: #d8dee9 var(--tw-gradient-from-position); 1047 | --tw-gradient-to: rgb(216 222 233 / 0) var(--tw-gradient-to-position); 1048 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 1049 | } 1050 | 1051 | .to-nord-light-green { 1052 | --tw-gradient-to: #8fbcbb var(--tw-gradient-to-position); 1053 | } 1054 | 1055 | .bg-clip-text { 1056 | -webkit-background-clip: text; 1057 | background-clip: text; 1058 | } 1059 | 1060 | .p-1 { 1061 | padding: 0.25rem; 1062 | } 1063 | 1064 | .p-2 { 1065 | padding: 0.5rem; 1066 | } 1067 | 1068 | .p-4 { 1069 | padding: 1rem; 1070 | } 1071 | 1072 | .p-6 { 1073 | padding: 1.5rem; 1074 | } 1075 | 1076 | .px-1 { 1077 | padding-left: 0.25rem; 1078 | padding-right: 0.25rem; 1079 | } 1080 | 1081 | .px-2 { 1082 | padding-left: 0.5rem; 1083 | padding-right: 0.5rem; 1084 | } 1085 | 1086 | .px-3 { 1087 | padding-left: 0.75rem; 1088 | padding-right: 0.75rem; 1089 | } 1090 | 1091 | .px-4 { 1092 | padding-left: 1rem; 1093 | padding-right: 1rem; 1094 | } 1095 | 1096 | .px-6 { 1097 | padding-left: 1.5rem; 1098 | padding-right: 1.5rem; 1099 | } 1100 | 1101 | .py-1 { 1102 | padding-top: 0.25rem; 1103 | padding-bottom: 0.25rem; 1104 | } 1105 | 1106 | .py-2 { 1107 | padding-top: 0.5rem; 1108 | padding-bottom: 0.5rem; 1109 | } 1110 | 1111 | .py-3 { 1112 | padding-top: 0.75rem; 1113 | padding-bottom: 0.75rem; 1114 | } 1115 | 1116 | .pb-16 { 1117 | padding-bottom: 4rem; 1118 | } 1119 | 1120 | .pb-4 { 1121 | padding-bottom: 1rem; 1122 | } 1123 | 1124 | .pl-1 { 1125 | padding-left: 0.25rem; 1126 | } 1127 | 1128 | .pr-2 { 1129 | padding-right: 0.5rem; 1130 | } 1131 | 1132 | .pr-5 { 1133 | padding-right: 1.25rem; 1134 | } 1135 | 1136 | .pt-20 { 1137 | padding-top: 5rem; 1138 | } 1139 | 1140 | .pt-4 { 1141 | padding-top: 1rem; 1142 | } 1143 | 1144 | .text-left { 1145 | text-align: left; 1146 | } 1147 | 1148 | .text-center { 1149 | text-align: center; 1150 | } 1151 | 1152 | .text-right { 1153 | text-align: right; 1154 | } 1155 | 1156 | .align-middle { 1157 | vertical-align: middle; 1158 | } 1159 | 1160 | .font-mono { 1161 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 1162 | } 1163 | 1164 | .text-2xl { 1165 | font-size: 1.5rem; 1166 | line-height: 2rem; 1167 | } 1168 | 1169 | .text-3xl { 1170 | font-size: 1.875rem; 1171 | line-height: 2.25rem; 1172 | } 1173 | 1174 | .text-4xl { 1175 | font-size: 2.25rem; 1176 | line-height: 2.5rem; 1177 | } 1178 | 1179 | .text-base { 1180 | font-size: 1rem; 1181 | line-height: 1.5rem; 1182 | } 1183 | 1184 | .text-lg { 1185 | font-size: 1.125rem; 1186 | line-height: 1.75rem; 1187 | } 1188 | 1189 | .text-sm { 1190 | font-size: 0.875rem; 1191 | line-height: 1.25rem; 1192 | } 1193 | 1194 | .text-xs { 1195 | font-size: 0.75rem; 1196 | line-height: 1rem; 1197 | } 1198 | 1199 | .font-bold { 1200 | font-weight: 700; 1201 | } 1202 | 1203 | .font-extrabold { 1204 | font-weight: 800; 1205 | } 1206 | 1207 | .font-medium { 1208 | font-weight: 500; 1209 | } 1210 | 1211 | .font-normal { 1212 | font-weight: 400; 1213 | } 1214 | 1215 | .font-semibold { 1216 | font-weight: 600; 1217 | } 1218 | 1219 | .not-italic { 1220 | font-style: normal; 1221 | } 1222 | 1223 | .leading-loose { 1224 | line-height: 2; 1225 | } 1226 | 1227 | .tracking-wide { 1228 | letter-spacing: 0.025em; 1229 | } 1230 | 1231 | .tracking-wider { 1232 | letter-spacing: 0.05em; 1233 | } 1234 | 1235 | .text-code-bg-black { 1236 | --tw-text-opacity: 1; 1237 | color: rgb(153 153 153 / var(--tw-text-opacity)); 1238 | } 1239 | 1240 | .text-gray-400 { 1241 | --tw-text-opacity: 1; 1242 | color: rgb(156 163 175 / var(--tw-text-opacity)); 1243 | } 1244 | 1245 | .text-gray-600 { 1246 | --tw-text-opacity: 1; 1247 | color: rgb(75 85 99 / var(--tw-text-opacity)); 1248 | } 1249 | 1250 | .text-gray-900 { 1251 | --tw-text-opacity: 1; 1252 | color: rgb(17 24 39 / var(--tw-text-opacity)); 1253 | } 1254 | 1255 | .text-nord-light-bg { 1256 | --tw-text-opacity: 1; 1257 | color: rgb(236 239 244 / var(--tw-text-opacity)); 1258 | } 1259 | 1260 | .text-nord-light-green { 1261 | --tw-text-opacity: 1; 1262 | color: rgb(143 188 187 / var(--tw-text-opacity)); 1263 | } 1264 | 1265 | .text-nord-light-sub { 1266 | --tw-text-opacity: 1; 1267 | color: rgb(106 119 145 / var(--tw-text-opacity)); 1268 | } 1269 | 1270 | .text-rose-400 { 1271 | --tw-text-opacity: 1; 1272 | color: rgb(251 113 133 / var(--tw-text-opacity)); 1273 | } 1274 | 1275 | .text-slate-400 { 1276 | --tw-text-opacity: 1; 1277 | color: rgb(148 163 184 / var(--tw-text-opacity)); 1278 | } 1279 | 1280 | .text-transparent { 1281 | color: transparent; 1282 | } 1283 | 1284 | .text-white { 1285 | --tw-text-opacity: 1; 1286 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1287 | } 1288 | 1289 | .underline { 1290 | text-decoration-line: underline; 1291 | } 1292 | 1293 | .opacity-60 { 1294 | opacity: 0.6; 1295 | } 1296 | 1297 | .opacity-70 { 1298 | opacity: 0.7; 1299 | } 1300 | 1301 | .shadow-sm { 1302 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 1303 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); 1304 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1305 | } 1306 | 1307 | .outline { 1308 | outline-style: solid; 1309 | } 1310 | 1311 | .transition { 1312 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; 1313 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 1314 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; 1315 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1316 | transition-duration: 150ms; 1317 | } 1318 | 1319 | .transition-opacity { 1320 | transition-property: opacity; 1321 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1322 | transition-duration: 150ms; 1323 | } 1324 | 1325 | .duration-300 { 1326 | transition-duration: 300ms; 1327 | } 1328 | 1329 | .animate-delay-\[150ms\] { 1330 | animation-delay: 150ms; 1331 | } 1332 | 1333 | .animate-delay-\[200ms\] { 1334 | animation-delay: 200ms; 1335 | } 1336 | 1337 | .animate-duration-1000 { 1338 | animation-duration: 1000ms; 1339 | } 1340 | 1341 | .animate-once { 1342 | animation-iteration-count: 1; 1343 | } 1344 | 1345 | .hover\:bg-nord-light-green:hover { 1346 | --tw-bg-opacity: 1; 1347 | background-color: rgb(143 188 187 / var(--tw-bg-opacity)); 1348 | } 1349 | 1350 | .hover\:text-nord-light-bg:hover { 1351 | --tw-text-opacity: 1; 1352 | color: rgb(236 239 244 / var(--tw-text-opacity)); 1353 | } 1354 | 1355 | .hover\:opacity-60:hover { 1356 | opacity: 0.6; 1357 | } 1358 | 1359 | .focus\:border-4:focus { 1360 | border-width: 4px; 1361 | } 1362 | 1363 | .focus\:border-opacity-60:focus { 1364 | --tw-border-opacity: 0.6; 1365 | } 1366 | 1367 | .focus\:outline-none:focus { 1368 | outline: 2px solid transparent; 1369 | outline-offset: 2px; 1370 | } 1371 | 1372 | @media (min-width: 640px) { 1373 | .sm\:ml-3 { 1374 | margin-left: 0.75rem; 1375 | } 1376 | 1377 | .sm\:mt-6 { 1378 | margin-top: 1.5rem; 1379 | } 1380 | 1381 | .sm\:flex { 1382 | display: flex; 1383 | } 1384 | 1385 | .sm\:text-4xl { 1386 | font-size: 2.25rem; 1387 | line-height: 2.5rem; 1388 | } 1389 | } 1390 | 1391 | @media (min-width: 768px) { 1392 | .md\:mx-0 { 1393 | margin-left: 0px; 1394 | margin-right: 0px; 1395 | } 1396 | 1397 | .md\:my-2 { 1398 | margin-top: 0.5rem; 1399 | margin-bottom: 0.5rem; 1400 | } 1401 | 1402 | .md\:mt-0 { 1403 | margin-top: 0px; 1404 | } 1405 | 1406 | .md\:mt-4 { 1407 | margin-top: 1rem; 1408 | } 1409 | 1410 | .md\:mt-8 { 1411 | margin-top: 2rem; 1412 | } 1413 | 1414 | .md\:block { 1415 | display: block; 1416 | } 1417 | 1418 | .md\:flex { 1419 | display: flex; 1420 | } 1421 | 1422 | .md\:h-16 { 1423 | height: 4rem; 1424 | } 1425 | 1426 | .md\:grid-cols-2 { 1427 | grid-template-columns: repeat(2, minmax(0, 1fr)); 1428 | } 1429 | 1430 | .md\:flex-col { 1431 | flex-direction: column; 1432 | } 1433 | 1434 | .md\:px-10 { 1435 | padding-left: 2.5rem; 1436 | padding-right: 2.5rem; 1437 | } 1438 | 1439 | .md\:text-3xl { 1440 | font-size: 1.875rem; 1441 | line-height: 2.25rem; 1442 | } 1443 | 1444 | .md\:text-5xl { 1445 | font-size: 3rem; 1446 | line-height: 1; 1447 | } 1448 | 1449 | .md\:text-base { 1450 | font-size: 1rem; 1451 | line-height: 1.5rem; 1452 | } 1453 | 1454 | .md\:text-lg { 1455 | font-size: 1.125rem; 1456 | line-height: 1.75rem; 1457 | } 1458 | } 1459 | 1460 | @media (min-width: 1024px) { 1461 | .lg\:my-4 { 1462 | margin-top: 1rem; 1463 | margin-bottom: 1rem; 1464 | } 1465 | 1466 | .lg\:mb-5 { 1467 | margin-bottom: 1.25rem; 1468 | } 1469 | 1470 | .lg\:ml-2 { 1471 | margin-left: 0.5rem; 1472 | } 1473 | 1474 | .lg\:mt-0 { 1475 | margin-top: 0px; 1476 | } 1477 | 1478 | .lg\:mt-5 { 1479 | margin-top: 1.25rem; 1480 | } 1481 | 1482 | .lg\:flex { 1483 | display: flex; 1484 | } 1485 | 1486 | .lg\:grid { 1487 | display: grid; 1488 | } 1489 | 1490 | .lg\:h-10 { 1491 | height: 2.5rem; 1492 | } 1493 | 1494 | .lg\:h-12 { 1495 | height: 3rem; 1496 | } 1497 | 1498 | .lg\:h-64 { 1499 | height: 16rem; 1500 | } 1501 | 1502 | .lg\:h-\[75\%\] { 1503 | height: 75%; 1504 | } 1505 | 1506 | .lg\:h-\[calc\(100\%-1\.25rem\)\] { 1507 | height: calc(100% - 1.25rem); 1508 | } 1509 | 1510 | .lg\:w-10 { 1511 | width: 2.5rem; 1512 | } 1513 | 1514 | .lg\:w-12 { 1515 | width: 3rem; 1516 | } 1517 | 1518 | .lg\:w-28 { 1519 | width: 7rem; 1520 | } 1521 | 1522 | .lg\:grid-cols-2 { 1523 | grid-template-columns: repeat(2, minmax(0, 1fr)); 1524 | } 1525 | 1526 | .lg\:grid-cols-3 { 1527 | grid-template-columns: repeat(3, minmax(0, 1fr)); 1528 | } 1529 | 1530 | .lg\:gap-4 { 1531 | gap: 1rem; 1532 | } 1533 | 1534 | .lg\:border { 1535 | border-width: 1px; 1536 | } 1537 | 1538 | .lg\:border-slate-200 { 1539 | --tw-border-opacity: 1; 1540 | border-color: rgb(226 232 240 / var(--tw-border-opacity)); 1541 | } 1542 | 1543 | .lg\:bg-slate-100 { 1544 | --tw-bg-opacity: 1; 1545 | background-color: rgb(241 245 249 / var(--tw-bg-opacity)); 1546 | } 1547 | 1548 | .lg\:p-12 { 1549 | padding: 3rem; 1550 | } 1551 | 1552 | .lg\:px-1 { 1553 | padding-left: 0.25rem; 1554 | padding-right: 0.25rem; 1555 | } 1556 | 1557 | .lg\:px-1\.5 { 1558 | padding-left: 0.375rem; 1559 | padding-right: 0.375rem; 1560 | } 1561 | 1562 | .lg\:px-14 { 1563 | padding-left: 3.5rem; 1564 | padding-right: 3.5rem; 1565 | } 1566 | 1567 | .lg\:py-0 { 1568 | padding-top: 0px; 1569 | padding-bottom: 0px; 1570 | } 1571 | 1572 | .lg\:py-0\.5 { 1573 | padding-top: 0.125rem; 1574 | padding-bottom: 0.125rem; 1575 | } 1576 | 1577 | .lg\:pt-16 { 1578 | padding-top: 4rem; 1579 | } 1580 | 1581 | .lg\:text-2xl { 1582 | font-size: 1.5rem; 1583 | line-height: 2rem; 1584 | } 1585 | 1586 | .lg\:text-base { 1587 | font-size: 1rem; 1588 | line-height: 1.5rem; 1589 | } 1590 | 1591 | .lg\:text-xl { 1592 | font-size: 1.25rem; 1593 | line-height: 1.75rem; 1594 | } 1595 | 1596 | .lg\:font-bold { 1597 | font-weight: 700; 1598 | } 1599 | 1600 | .lg\:leading-\[3rem\] { 1601 | line-height: 3rem; 1602 | } 1603 | 1604 | .lg\:hover\:bg-nord-light-green:hover { 1605 | --tw-bg-opacity: 1; 1606 | background-color: rgb(143 188 187 / var(--tw-bg-opacity)); 1607 | } 1608 | 1609 | .lg\:hover\:text-nord-light-bg:hover { 1610 | --tw-text-opacity: 1; 1611 | color: rgb(236 239 244 / var(--tw-text-opacity)); 1612 | } 1613 | 1614 | .lg\:hover\:opacity-60:hover { 1615 | opacity: 0.6; 1616 | } 1617 | 1618 | .group:hover .lg\:group-hover\:block { 1619 | display: block; 1620 | } 1621 | } 1622 | 1623 | @media (min-width: 1280px) { 1624 | .xl\:grid-cols-4 { 1625 | grid-template-columns: repeat(4, minmax(0, 1fr)); 1626 | } 1627 | 1628 | .xl\:text-6xl { 1629 | font-size: 3.75rem; 1630 | line-height: 1; 1631 | } 1632 | 1633 | .xl\:text-lg { 1634 | font-size: 1.125rem; 1635 | line-height: 1.75rem; 1636 | } 1637 | } 1638 | -------------------------------------------------------------------------------- /public/style/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+yaml&plugins=line-highlight+line-numbers+show-language+toolbar+copy-to-clipboard */ 3 | code[class*="language-"], 4 | pre[class*="language-"] { 5 | color: #000; 6 | background: 0 0; 7 | text-shadow: 0 1px #fff; 8 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 9 | font-size: 1em; 10 | text-align: left; 11 | white-space: pre; 12 | word-spacing: normal; 13 | word-break: normal; 14 | word-wrap: normal; 15 | line-height: 1.5; 16 | -moz-tab-size: 4; 17 | -o-tab-size: 4; 18 | tab-size: 4; 19 | -webkit-hyphens: none; 20 | -moz-hyphens: none; 21 | -ms-hyphens: none; 22 | hyphens: none; 23 | } 24 | 25 | code[class*="language-"] ::-moz-selection, 26 | code[class*="language-"]::-moz-selection, 27 | pre[class*="language-"] ::-moz-selection, 28 | pre[class*="language-"]::-moz-selection { 29 | text-shadow: none; 30 | background: #b3d4fc; 31 | } 32 | 33 | code[class*="language-"] ::selection, 34 | code[class*="language-"]::selection, 35 | pre[class*="language-"] ::selection, 36 | pre[class*="language-"]::selection { 37 | text-shadow: none; 38 | background: #b3d4fc; 39 | } 40 | 41 | @media print { 42 | code[class*="language-"], 43 | pre[class*="language-"] { 44 | text-shadow: none; 45 | } 46 | } 47 | 48 | pre[class*="language-"] { 49 | padding: 1em; 50 | margin: 0.5em 0; 51 | overflow: auto; 52 | } 53 | 54 | :not(pre) > code[class*="language-"], 55 | pre[class*="language-"] { 56 | background: #f5f2f0; 57 | } 58 | 59 | :not(pre) > code[class*="language-"] { 60 | padding: 0.1em; 61 | border-radius: 0.3em; 62 | white-space: normal; 63 | } 64 | 65 | .token.cdata, 66 | .token.comment, 67 | .token.doctype, 68 | .token.prolog { 69 | color: #708090; 70 | } 71 | 72 | .token.punctuation { 73 | color: #999; 74 | } 75 | 76 | .token.namespace { 77 | opacity: 0.7; 78 | } 79 | 80 | .token.boolean, 81 | .token.constant, 82 | .token.deleted, 83 | .token.number, 84 | .token.property, 85 | .token.symbol, 86 | .token.tag { 87 | color: #905; 88 | } 89 | 90 | .token.attr-name, 91 | .token.builtin, 92 | .token.char, 93 | .token.inserted, 94 | .token.selector, 95 | .token.string { 96 | color: #690; 97 | } 98 | 99 | .language-css .token.string, 100 | .style .token.string, 101 | .token.entity, 102 | .token.operator, 103 | .token.url { 104 | color: #9a6e3a; 105 | background: hsla(0, 0%, 100%, 0.5); 106 | } 107 | 108 | .token.atrule, 109 | .token.attr-value, 110 | .token.keyword { 111 | color: #07a; 112 | } 113 | 114 | .token.class-name, 115 | .token.function { 116 | color: #dd4a68; 117 | } 118 | 119 | .token.important, 120 | .token.regex, 121 | .token.variable { 122 | color: #e90; 123 | } 124 | 125 | .token.bold, 126 | .token.important { 127 | font-weight: 700; 128 | } 129 | 130 | .token.italic { 131 | font-style: italic; 132 | } 133 | 134 | .token.entity { 135 | cursor: help; 136 | } 137 | 138 | pre[data-line] { 139 | position: relative; 140 | padding: 1em 0 1em 3em; 141 | } 142 | 143 | .line-highlight { 144 | position: absolute; 145 | left: 0; 146 | right: 0; 147 | padding: inherit 0; 148 | margin-top: 1em; 149 | background: hsla(24, 20%, 50%, 0.08); 150 | background: linear-gradient( 151 | to right, 152 | hsla(24, 20%, 50%, 0.1) 70%, 153 | hsla(24, 20%, 50%, 0) 154 | ); 155 | pointer-events: none; 156 | line-height: inherit; 157 | white-space: pre; 158 | } 159 | 160 | @media print { 161 | .line-highlight { 162 | -webkit-print-color-adjust: exact; 163 | color-adjust: exact; 164 | } 165 | } 166 | 167 | .line-highlight:before, 168 | .line-highlight[data-end]:after { 169 | content: attr(data-start); 170 | position: absolute; 171 | top: 0.4em; 172 | left: 0.6em; 173 | min-width: 1em; 174 | padding: 0 0.5em; 175 | background-color: hsla(24, 20%, 50%, 0.4); 176 | color: #f4f1ef; 177 | font: bold 65%/1.5 sans-serif; 178 | text-align: center; 179 | vertical-align: 0.3em; 180 | border-radius: 999px; 181 | text-shadow: none; 182 | box-shadow: 0 1px #fff; 183 | } 184 | 185 | .line-highlight[data-end]:after { 186 | content: attr(data-end); 187 | top: auto; 188 | bottom: 0.4em; 189 | } 190 | 191 | .line-numbers .line-highlight:after, 192 | .line-numbers .line-highlight:before { 193 | content: none; 194 | } 195 | 196 | pre[id].linkable-line-numbers span.line-numbers-rows { 197 | pointer-events: all; 198 | } 199 | 200 | pre[id].linkable-line-numbers span.line-numbers-rows > span:before { 201 | cursor: pointer; 202 | } 203 | 204 | pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before { 205 | background-color: rgba(128, 128, 128, 0.2); 206 | } 207 | 208 | pre[class*="language-"].line-numbers { 209 | position: relative; 210 | padding-left: 3em; 211 | counter-reset: linenumber; 212 | } 213 | 214 | pre[class*="language-"].line-numbers > code { 215 | position: relative; 216 | white-space: inherit; 217 | } 218 | 219 | .line-numbers .line-numbers-rows { 220 | position: absolute; 221 | pointer-events: none; 222 | top: 24px; 223 | font-size: 100%; 224 | left: -16em; 225 | width: 3em; 226 | letter-spacing: -1px; 227 | /* border-right: 1px solid #999; */ 228 | -webkit-user-select: none; 229 | -moz-user-select: none; 230 | -ms-user-select: none; 231 | user-select: none; 232 | } 233 | 234 | .line-numbers-rows > span { 235 | display: block; 236 | counter-increment: linenumber; 237 | } 238 | 239 | .line-numbers-rows > span:before { 240 | content: counter(linenumber); 241 | color: #999; 242 | display: block; 243 | padding-right: 0.8em; 244 | text-align: right; 245 | } 246 | 247 | div.code-toolbar { 248 | position: relative; 249 | } 250 | 251 | div.code-toolbar > .toolbar { 252 | position: absolute; 253 | z-index: 10; 254 | top: 0.3em; 255 | right: 0.2em; 256 | transition: opacity 0.3s ease-in-out; 257 | opacity: 0; 258 | } 259 | 260 | div.code-toolbar:hover > .toolbar { 261 | opacity: 1; 262 | } 263 | 264 | div.code-toolbar:focus-within > .toolbar { 265 | opacity: 1; 266 | } 267 | 268 | div.code-toolbar > .toolbar > .toolbar-item { 269 | display: inline-block; 270 | } 271 | 272 | div.code-toolbar > .toolbar > .toolbar-item > a { 273 | cursor: pointer; 274 | } 275 | 276 | div.code-toolbar > .toolbar > .toolbar-item > button { 277 | background: 0 0; 278 | border: 0; 279 | color: inherit; 280 | font: inherit; 281 | line-height: normal; 282 | overflow: visible; 283 | padding: 0; 284 | -webkit-user-select: none; 285 | -moz-user-select: none; 286 | -ms-user-select: none; 287 | } 288 | 289 | div.code-toolbar > .toolbar > .toolbar-item > a, 290 | div.code-toolbar > .toolbar > .toolbar-item > button, 291 | div.code-toolbar > .toolbar > .toolbar-item > span { 292 | color: #bbb; 293 | font-size: 0.8em; 294 | margin: 5px; 295 | padding: 10px 20px; 296 | background: #f5f2f0; 297 | background: rgb(255, 255, 255); 298 | box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); 299 | border-radius: 0.75em; 300 | } 301 | 302 | div.code-toolbar > .toolbar > .toolbar-item > a:focus, 303 | div.code-toolbar > .toolbar > .toolbar-item > a:hover, 304 | div.code-toolbar > .toolbar > .toolbar-item > button:focus, 305 | div.code-toolbar > .toolbar > .toolbar-item > button:hover, 306 | div.code-toolbar > .toolbar > .toolbar-item > span:focus, 307 | div.code-toolbar > .toolbar > .toolbar-item > span:hover { 308 | color: inherit; 309 | text-decoration: none; 310 | } 311 | -------------------------------------------------------------------------------- /public/views/favicon.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | MonkeyType Favicon 18 | 19 | 20 | 21 | 22 |
23 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /public/views/logo.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | MonkeyType Logo 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /public/views/user.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | <%= data.userId %> | MonkeyType Readme 23 | 27 | 28 | 29 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 47 | 51 | 55 | 59 | 60 | 61 | 62 | 66 | 70 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 124 | 125 | 126 | 127 | 456 | 457 | 458 | 459 | 460 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./*.{html,js}", "./public/views/*.html", "./public/views/*.ejs"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | "nord-light-green": "#8fbcbb", 8 | "nord-light-bg": "#eceff4", 9 | "nord-light-subAlt": "#d8dee9", 10 | "nord-light-sub": "#6a7791", 11 | "nord-light-error": "#bf616a", 12 | "code-bg-brown": "#f5f2f0", 13 | "code-bg-black": "#999", 14 | }, 15 | width: { 16 | 26: "6.5rem", 17 | 33: "8.25rem", 18 | 34: "8.5rem", 19 | }, 20 | margin: { 21 | 38: "9.5rem", 22 | }, 23 | animation: { 24 | "rgb-bg": "rgb-bg 10s linear infinite", 25 | }, 26 | keyframes: { 27 | "rgb-bg": { 28 | "0%": { "background-color": "hsl(120, 39%, 49%)" }, 29 | "20%": { "background-color": "hsl(192, 48%, 48%)" }, 30 | "40%": { "background-color": "hsl(264, 90%, 58%)" }, 31 | "60%": { "background-color": "hsl(357, 89%, 50%)" }, 32 | "80%": { "background-color": "hsl(46, 100%, 51%)" }, 33 | "100%": { "background-color": "hsl(120, 39%, 49%)" }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | plugins: [require("tailwindcss-animated")], 39 | }; 40 | --------------------------------------------------------------------------------