├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── app.d.ts ├── app.html ├── globals.css ├── hooks.server.ts ├── lib │ ├── components │ │ ├── dropdown.svelte │ │ ├── editor.svelte │ │ ├── footer.svelte │ │ ├── help-popup.svelte │ │ ├── login.svelte │ │ ├── navbar.svelte │ │ ├── popover.svelte │ │ ├── settings-popup.svelte │ │ ├── spinner.svelte │ │ └── test-settings.svelte │ ├── db │ │ ├── stats.ts │ │ └── update.ts │ ├── editor │ │ ├── ascii.ts │ │ ├── monaco.ts │ │ └── theme.ts │ ├── stores │ │ ├── persistent.ts │ │ ├── settings │ │ │ └── settings.ts │ │ └── test │ │ │ ├── options.ts │ │ │ ├── rounds.ts │ │ │ ├── scores.ts │ │ │ ├── status.ts │ │ │ └── timer.ts │ ├── test │ │ ├── constants.ts │ │ ├── options.ts │ │ └── tests │ │ │ ├── containers.ts │ │ │ ├── horizontal.ts │ │ │ ├── index.ts │ │ │ ├── lines.ts │ │ │ ├── mixed.ts │ │ │ └── movement.ts │ └── types │ │ ├── profile.ts │ │ ├── supabase.ts │ │ └── test.ts └── routes │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── about │ └── +page.svelte │ ├── account │ ├── create │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── settings │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── api │ ├── login │ │ └── +page.server.ts │ ├── logout │ │ └── +server.ts │ └── stats │ │ ├── increment │ │ └── +server.ts │ │ └── update │ │ └── +server.ts │ ├── auth │ └── callback │ │ └── +server.ts │ ├── privacy │ └── +page.svelte │ └── profile │ └── [user] │ ├── +page.server.ts │ └── +page.svelte ├── static ├── favicon.svg └── vimaroo-demo.gif ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_SUPABASE_URL="" 2 | PUBLIC_SUPABASE_ANON_KEY="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:svelte/recommended" 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | plugins: ["@typescript-eslint"], 11 | parserOptions: { 12 | sourceType: "module", 13 | ecmaVersion: 2020, 14 | extraFileExtensions: [".svelte"] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ["*.svelte"], 24 | parser: "svelte-eslint-parser", 25 | parserOptions: { 26 | parser: "@typescript-eslint/parser" 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": false, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tomas Oh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vimaroo 2 | 3 | [![vimaroo website](static/vimaroo-demo.gif)](https://vimaroo.vercel.app) 4 | 5 | ![Svelte](https://img.shields.io/badge/svelte-%23f1413d.svg?style=for-the-badge&logo=svelte&logoColor=white) 6 | ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 7 | ![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) 8 | ![Supabase](https://img.shields.io/badge/Supabase-3ECF8E?style=for-the-badge&logo=supabase&logoColor=white) 9 | ![Vim](https://img.shields.io/badge/VIM-%2311AB00.svg?style=for-the-badge&logo=vim&logoColor=white) 10 | 11 | ## About 12 | 13 | This website was created with the intent of making it easy to practice Vim keybinds with a set of motion-focused test. Inspired by ThePrimeagen's [vim-be-good](https://github.com/ThePrimeagen/vim-be-good) Neovim plugin and [Monkeytype](https://monkeytype.com/). 14 | 15 | ## Starting tests 16 | 17 | Select a test to begin. Locate and delete the specified line using `dd` to start the test. If you need to exit early, use `:q`. Follow the instructions and complete each test using Vim motions! 18 | 19 | ## Setup / Development 20 | 21 | Create a [Supabase](https://supabase.com/) project. To store profiles and user stats, you will need to create those corresponding tables in Supabase (check [supabase.ts](https://github.com/tomasohCHOM/vimaroo/blob/main/src/lib/types/supabase.ts) to get an idea of how the Postgres schemas look). 22 | 23 | Clone the repository and install all the dependencies: 24 | 25 | ```bash 26 | git clone https://github.com/tomasohCHOM/vimaroo.git 27 | cd vimaroo 28 | npm install 29 | ``` 30 | 31 | Create a new `.env`, following the `.env.example` template for the keys needed to connect to Supabase: 32 | 33 | ```env 34 | PUBLIC_SUPABASE_URL="" 35 | PUBLIC_SUPABASE_ANON_KEY="" 36 | ``` 37 | 38 | Then start the development process: 39 | 40 | ```bash 41 | npm run dev 42 | ``` 43 | 44 | To build and start in production mode: 45 | 46 | ```bash 47 | npm run build 48 | npm run preview 49 | ``` 50 | 51 | --- 52 | 53 | Developed with 🔥 by [Chom.](https://github.com/tomasohCHOM) 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vimaroo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --check . && eslint .", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@iconify/svelte": "^4.0.2", 16 | "@sveltejs/adapter-auto": "^3.0.0", 17 | "@sveltejs/kit": "^2.0.0", 18 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 19 | "@types/eslint": "^8.56.0", 20 | "@typescript-eslint/eslint-plugin": "^7.0.0", 21 | "@typescript-eslint/parser": "^7.0.0", 22 | "autoprefixer": "^10.4.19", 23 | "cjs": "^0.0.11", 24 | "eslint": "^8.56.0", 25 | "eslint-plugin-svelte": "^2.35.1", 26 | "postcss": "^8.4.38", 27 | "prettier": "^3.3.3", 28 | "prettier-plugin-svelte": "^3.2.6", 29 | "prettier-plugin-tailwindcss": "^0.6.5", 30 | "svelte": "^4.2.7", 31 | "svelte-check": "^3.6.0", 32 | "sveltekit-flash-message": "^2.4.4", 33 | "tailwindcss": "^3.4.3", 34 | "tslib": "^2.4.1", 35 | "typescript": "^5.0.0", 36 | "vite": "^5.0.3" 37 | }, 38 | "type": "module", 39 | "dependencies": { 40 | "@supabase/ssr": "^0.4.0", 41 | "@supabase/supabase-js": "^2.45.1", 42 | "monaco-editor": "^0.48.0", 43 | "monaco-vim": "^0.4.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | 3 | import type { Database } from "$lib/types/supabase"; 4 | import type { SupabaseClient, Session, User } from "@supabase/supabase-js"; 5 | 6 | // for information about these interfaces 7 | declare global { 8 | namespace App { 9 | // interface Error {} 10 | interface Locals { 11 | supabase: SupabaseClient; 12 | safeGetSession: () => Promise<{ session: Session | null; user: User | null }>; 13 | session: Session | null; 14 | user: User | null; 15 | } 16 | interface PageData { 17 | session: Session | null; 18 | flash?: { type: "success" | "error"; message: string }; 19 | } 20 | // interface PageState {} 21 | // interface Platform {} 22 | } 23 | } 24 | 25 | export {}; 26 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap"); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | :root { 9 | --ff-general: "Lato", sans-serif; 10 | --ff-code: "Fira Code", monospace; 11 | 12 | --color-background-400: 32, 35, 44; 13 | --color-background-500: 37, 41, 50; 14 | --color-background-600: 52, 55, 69; 15 | 16 | --color-foreground-neutral: 255, 255, 255; 17 | --color-foreground-red: 224, 108, 117; 18 | --color-foreground-green: 152, 195, 121; 19 | --color-foreground-blue: 97, 175, 239; 20 | 21 | background-color: rgb(var(--color-background-500)); 22 | color: rgb(var(--color-foreground-neutral)); 23 | 24 | color-scheme: dark; 25 | } 26 | 27 | *, 28 | *::before, 29 | *::after { 30 | box-sizing: border-box; 31 | } 32 | 33 | html { 34 | transition: all 0.125s linear; 35 | } 36 | 37 | body { 38 | margin: 0; 39 | padding: 0; 40 | min-height: 100vh; 41 | font-family: var(--ff-code); 42 | } 43 | 44 | #app { 45 | padding-block: 2rem; 46 | min-height: 100vh; 47 | display: flex; 48 | flex-direction: column; 49 | row-gap: 1rem; 50 | margin-inline: auto; 51 | } 52 | 53 | code { 54 | font-family: var(--ff-code); 55 | } 56 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient } from "@supabase/ssr"; 2 | import { type Handle, redirect } from "@sveltejs/kit"; 3 | import { sequence } from "@sveltejs/kit/hooks"; 4 | 5 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from "$env/static/public"; 6 | 7 | const supabase: Handle = async ({ event, resolve }) => { 8 | /** 9 | * Creates a Supabase client specific to this server request. 10 | * 11 | * The Supabase client gets the Auth token from the request cookies. 12 | */ 13 | event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { 14 | cookies: { 15 | getAll: () => event.cookies.getAll(), 16 | /** 17 | * SvelteKit's cookies API requires `path` to be explicitly set in 18 | * the cookie options. Setting `path` to `/` replicates previous/ 19 | * standard behavior. 20 | */ 21 | setAll: (cookiesToSet) => { 22 | cookiesToSet.forEach(({ name, value, options }) => { 23 | event.cookies.set(name, value, { ...options, path: "/" }); 24 | }); 25 | } 26 | } 27 | }); 28 | 29 | if ("suppressGetSessionWarning" in event.locals.supabase.auth) { 30 | // @ts-expect-error - suppressGetSessionWarning is not part of the official API 31 | event.locals.supabase.auth.suppressGetSessionWarning = true; 32 | } else { 33 | console.warn( 34 | "SupabaseAuthClient#suppressGetSessionWarning was removed. See https://github.com/supabase/auth-js/issues/888." 35 | ); 36 | } 37 | 38 | /** 39 | * Unlike `supabase.auth.getSession()`, which returns the session _without_ 40 | * validating the JWT, this function also calls `getUser()` to validate the 41 | * JWT before returning the session. 42 | */ 43 | event.locals.safeGetSession = async () => { 44 | const { 45 | data: { session } 46 | } = await event.locals.supabase.auth.getSession(); 47 | if (!session) { 48 | return { session: null, user: null }; 49 | } 50 | 51 | const { 52 | data: { user }, 53 | error 54 | } = await event.locals.supabase.auth.getUser(); 55 | if (error) { 56 | // JWT validation has failed 57 | return { session: null, user: null }; 58 | } 59 | 60 | return { session, user }; 61 | }; 62 | 63 | return resolve(event, { 64 | filterSerializedResponseHeaders(name) { 65 | /** 66 | * Supabase libraries use the `content-range` and `x-supabase-api-version` 67 | * headers, so we need to tell SvelteKit to pass it through. 68 | */ 69 | return name === "content-range" || name === "x-supabase-api-version"; 70 | } 71 | }); 72 | }; 73 | 74 | const authGuard: Handle = async ({ event, resolve }) => { 75 | const { session, user } = await event.locals.safeGetSession(); 76 | event.locals.session = session; 77 | event.locals.user = user; 78 | 79 | if (!event.locals.session && event.url.pathname.startsWith("/private")) { 80 | redirect(303, "/auth"); 81 | } 82 | 83 | if (event.locals.session && event.url.pathname === "/auth") { 84 | redirect(303, "/private"); 85 | } 86 | 87 | return resolve(event); 88 | }; 89 | 90 | export const handle: Handle = sequence(supabase, authGuard); 91 | -------------------------------------------------------------------------------- /src/lib/components/dropdown.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
30 |
(isDropdownOpen = true)} 34 | on:keydown={() => (isDropdownOpen = true)} 35 | aria-pressed="false" 36 | tabindex="0" 37 | > 38 | {dropdownOptions[selectedOption]} 39 | 40 |
41 |
46 | {#each dropdownOptions as option, i} 47 | 59 | {/each} 60 |
61 |
62 | -------------------------------------------------------------------------------- /src/lib/components/editor.svelte: -------------------------------------------------------------------------------- 1 | 210 | 211 | {#if !loaded} 212 |
213 | 214 |
215 | {/if} 216 | 217 |
218 |
219 |

223 |

Tip: You can reset tests by using :q

224 |
225 | -------------------------------------------------------------------------------- /src/lib/components/footer.svelte: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/lib/components/help-popup.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 |

Help❓

13 |

14 | A brief guide to the tests in vimaroo. Start by selecting a test, then 15 | locate and delete the specified line using dd to start the test. If you need to 16 | exit early, use :q. Follow the instructions and complete each test using Vim 17 | motions! 18 |

19 |
20 |
21 |

Horizontal ⬅️➡️

22 |

23 | Remove the special character ('*', '#', '@', etc.) from the sequence of words. You can move 24 | between words with w (go one word forward) or b (go one word 25 | backward). You can also use f/F to go directly to that character, 26 | or ^ to go to the beginning of the line, and $ to go to the end of 27 | the line. 28 |

29 |
30 |
31 |

Containers 🫙

32 |

33 | Delete the inside contents of the container. You can do this with the di 34 | command followed by the opening character of the container ("delete inside" the container). For 35 | example, use di" to delete everything inside the double quotes. 36 |

37 |
38 |
39 |

Lines ⬆️⬇️

40 |

41 | Delete the line with the sentence (you can ONLY delete that single line). Move up and down 42 | with k and j, but relative line jumping can be helpful as well 43 | (e.g., 6j will go down 6 lines). 44 |

45 |
46 |
47 |

Movement ⬅️⬇️⬆️➡️

48 |

49 | Move within the dot grid with hjkl 50 | and remove the special character ('*', '#', '@', etc.) using x. If it's faster, 51 | you can also search for the character itself using /. 52 |

53 |
54 |
55 |

Mixed 🥗

56 |

57 | All tests combined into one! Any of the tests above will be randomly chosen for each round 58 | (horizontal, containers, lines, or movement tests). 59 |

60 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /src/lib/components/login.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |

User Login

10 |

Continue with one of the following social login providers

11 | 12 |
13 | 21 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/lib/components/navbar.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 77 | 78 | 91 | -------------------------------------------------------------------------------- /src/lib/components/popover.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#key isOpen} 17 | 32 | 33 |