├── .eslintrc.json ├── .gitignore ├── .hintrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── app ├── appData.tsx ├── apps │ └── [id] │ │ └── page.tsx ├── components │ ├── About.tsx │ ├── Apps.tsx │ ├── Cta.tsx │ ├── Faq.tsx │ ├── Featured.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── ModeToggle.tsx │ ├── ProjectComponent.tsx │ ├── ProjectDetail.tsx │ ├── projects │ │ ├── ColorGenerator.tsx │ │ ├── GradientGenerator.tsx │ │ ├── NumberGenerator.tsx │ │ ├── StringGenerator.tsx │ │ └── ThemeSwitcher.tsx │ └── theme-provider.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components └── ui │ ├── button.tsx │ └── dropdown-menu.tsx ├── lib └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── avatar.png ├── icons │ ├── master.svg │ ├── musicplayer.svg │ ├── ninja.svg │ ├── novice.svg │ ├── pro.svg │ ├── randomcolor.svg │ ├── randomgradient.svg │ ├── randomnumber.svg │ ├── randomstring.svg │ ├── rookie.svg │ ├── tetris.svg │ ├── themeswitcher.svg │ └── tictactoe.svg ├── logo.png ├── next.svg └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "axe/name-role-value": [ 7 | "default", 8 | { 9 | "link-name": "off" 10 | } 11 | ], 12 | "no-inline-styles": "off", 13 | "compat-api/css": [ 14 | "default", 15 | { 16 | "ignore": [ 17 | "backdrop-filter" 18 | ] 19 | } 20 | ], 21 | "typescript-config/consistent-casing": "off", 22 | "axe/forms": [ 23 | "default", 24 | { 25 | "label": "off" 26 | } 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👋 Hello and thank you for considering contributing to `js-apps`! 👨‍💻👩‍💻 4 | 5 | ## How to Contribute 6 | 7 | There are many ways you can contribute to this project! Some of them include: 8 | 9 | 🐞 **Reporting bugs:** If you find any bugs in the code, please open an issue describing the problem and how we can reproduce it. 10 | 11 | 🎁 **Improving documentation:** If you believe the documentation can be enhanced or if you find any errors, please open an issue or submit a pull request. 12 | 13 | 💻 **Writing code:** If you enjoy programming and want to add a new feature, please fork the repository and submit a pull request. 14 | 15 | 🤔 **Offering ideas and suggestions:** If you have any ideas or suggestions to improve the project, please create an issue to discuss it. 16 | 17 | ## Contribution Guide 18 | 19 | Before you start contributing, make sure to follow these guidelines: 20 | 21 | 👥 Ensure your code is readable and easy to understand for other contributors. 22 | 23 | 👨‍👩‍👧‍👦 If you are going to submit a pull request, make sure it includes a detailed description of the changes you have made. 24 | 25 | 👍 Follow the coding standards and project conventions. 26 | 27 | 🚨 Ensure that unit tests pass successfully before submitting your pull request. 28 | 29 | ## How to Submit a Pull Request 30 | 31 | 1. Fork the repository. 32 | 2. Create a new branch with a descriptive name (e.g., `my-new-feature`). 33 | 3. Make your changes in this branch and ensure you follow the contribution guidelines. 34 | 4. Submit a pull request to the main branch of the repository. 35 | 5. Make sure your pull request includes a detailed description of the changes you have made. 36 | 37 | Thanks again for considering contributing to `js-apps`! 👏 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Hernando Abella 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![JS-APPS-06-02-2025_08_19_PM](https://github.com/user-attachments/assets/d49c57bf-3c58-4af3-b261-f914e86bc17d) 2 | 3 | ## How to Contribute? ✨ 4 | Please check our [contribution guide](./CONTRIBUTING.md) for more details if you want to contribute. 5 | 6 | ## Contact 📩 7 | If you have any questions or comments about this project, you can contact me at: hernandoabella@gmail.com 8 | 9 | Made with ❤️ by [@hernandoabella](https://github.com/hernandoabella) 10 | -------------------------------------------------------------------------------- /app/appData.tsx: -------------------------------------------------------------------------------- 1 | import RandomNumber from "@/app/components/projects/NumberGenerator"; 2 | import RandomString from "@/app/components/projects/StringGenerator"; 3 | import RandomColor from "./components/projects/ColorGenerator"; 4 | import RandomGradient from "./components/projects/GradientGenerator"; 5 | import ThemeSwitcher from "./components/projects/ThemeSwitcher"; 6 | 7 | export interface App { 8 | name: string; 9 | path: string; 10 | icon: string; 11 | ProjectComponent: React.FC | null; 12 | level: number; 13 | title: string; 14 | description: string; 15 | htmlSnippet?: string; 16 | cssSnippet?: string; 17 | jsSnippet?: string; 18 | projectStars: number; 19 | downloadLink: string; 20 | } 21 | 22 | export const apps: App[] = [ 23 | { 24 | name: "Number Generator", 25 | path: "/apps/number-generator", 26 | icon: "/icons/randomnumber.svg", 27 | ProjectComponent: RandomNumber, 28 | level: 1, 29 | title: "Number Generator", 30 | description: 31 | "Generate a random number between 1 and 100.", 32 | htmlSnippet: `<div class="container"> 33 | <div class="box"> 34 | <p id="randomNumber" class="number">7</p> 35 | <button id="generateButton" class="generate-btn">Random Number</button> 36 | </div> 37 | </div>`, 38 | cssSnippet: `@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); 39 | 40 | * { 41 | margin: 0; 42 | padding: 0; 43 | box-sizing: border-box; 44 | font-family: "Inter", sans-serif; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | height: 100vh; 52 | } 53 | 54 | .box { 55 | padding: 20px; 56 | border-radius: 10px; 57 | border: 2px solid #E4E4E7; 58 | text-align: center; 59 | } 60 | 61 | .number { 62 | font-size: 1.5em; 63 | font-weight: bold; 64 | margin-bottom: 20px; 65 | } 66 | 67 | button { 68 | padding: 10px 20px; 69 | background-color: #6B7280; 70 | color: white; 71 | border: none; 72 | border-radius: 5px; 73 | cursor: pointer; 74 | } 75 | 76 | button:hover { 77 | background-color: #4b515c; 78 | }`, 79 | jsSnippet: `// Function to generate a random number between 1 and 100 80 | const generateRandomNumber = () => { 81 | return Math.floor(Math.random() * 100) + 1; 82 | }; 83 | 84 | // Get references to the button and number display 85 | const generateButton = document.getElementById('generateButton'); 86 | const randomNumberDisplay = document.getElementById('randomNumber'); 87 | 88 | // Add event listener to the button to generate a new number on click 89 | generateButton.addEventListener('click', () => { 90 | const newRandomNumber = generateRandomNumber(); 91 | randomNumberDisplay.textContent = newRandomNumber; 92 | });`, 93 | 94 | projectStars: 0, 95 | downloadLink: 96 | "#", 97 | }, 98 | { 99 | name: "Color Generator", 100 | path: "/apps/color-generator", 101 | icon: "/icons/randomcolor.svg", 102 | ProjectComponent: RandomColor, 103 | level: 1, 104 | title: "Random Color Generator", 105 | description: 106 | "A simple web app that generates a random hex color with each button click.", 107 | htmlSnippet: `<!DOCTYPE html> 108 | <html lang="en"> 109 | 110 | <head> 111 | <meta charset="UTF-8"> 112 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 113 | <title>Random Color Generator</title> 114 | <link rel="stylesheet" href="/styles.css"> 115 | </head> 116 | 117 | <body> 118 | <div class="container"> 119 | <div class="color-box"> 120 | <p id="colorDisplay" class="color-text">#FF5733</p> 121 | <button id="colorButton" class="color-button">Random Color</button> 122 | </div> 123 | </div> 124 | 125 | <script src="/script.js"></script> 126 | </body> 127 | 128 | </html>`, 129 | cssSnippet: `@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); 130 | 131 | * { 132 | margin: 0; 133 | padding: 0; 134 | box-sizing: border-box; 135 | font-family: "Inter", sans-serif; 136 | } 137 | 138 | .container { 139 | display: flex; 140 | justify-content: center; 141 | align-items: center; 142 | height: 100vh; 143 | } 144 | 145 | .color-box { 146 | padding: 20px; 147 | border-radius: 10px; 148 | text-align: center; 149 | border: 2px solid #E4E4E7; 150 | } 151 | 152 | .color-text { 153 | font-size: 1.5rem; 154 | padding: 20px; 155 | border-radius: 5px; 156 | } 157 | 158 | button { 159 | padding: 10px 20px; 160 | background-color: #6B7280; 161 | color: white; 162 | border: none; 163 | border-radius: 5px; 164 | cursor: pointer; 165 | margin-top: 20px; 166 | width: 100%; 167 | } 168 | 169 | button:hover { 170 | background-color: #4b515c; 171 | }`, 172 | jsSnippet: `// Function to generate a random color in hex format 173 | const generateRandomColor = () => { 174 | const letters = "0123456789ABCDEF"; 175 | let color = "#"; 176 | for (let i = 0; i < 6; i++) { 177 | color += letters[Math.floor(Math.random() * 16)]; 178 | } 179 | return color; 180 | }; 181 | 182 | // Function to determine if a color is light or dark for contrast 183 | const isLightColor = (color) => { 184 | const r = parseInt(color.slice(1, 3), 16); 185 | const g = parseInt(color.slice(3, 5), 16); 186 | const b = parseInt(color.slice(5, 7), 16); 187 | // Use the luminance formula to determine brightness 188 | const luminance = 0.299 * r + 0.587 * g + 0.114 * b; 189 | return luminance > 186; // If luminance > 186, it's a light color 190 | }; 191 | 192 | // Function to update the color display 193 | const updateColorDisplay = (color) => { 194 | const colorDisplay = document.getElementById('colorDisplay'); 195 | colorDisplay.textContent = color; 196 | colorDisplay.style.backgroundColor = color; 197 | colorDisplay.style.color = isLightColor(color) ? 'black' : 'white'; 198 | }; 199 | 200 | // Function to generate a random color when the button is clicked 201 | document.getElementById('colorButton').addEventListener('click', () => { 202 | const newRandomColor = generateRandomColor(); 203 | updateColorDisplay(newRandomColor); 204 | }); 205 | 206 | // Initialize with a default color 207 | updateColorDisplay('#FF5733'); 208 | `, 209 | 210 | projectStars: 0, 211 | downloadLink: 212 | "#", 213 | }, 214 | 215 | { 216 | name: "String Generator", 217 | path: "/apps/string-generator", 218 | icon: "/icons/randomstring.svg", 219 | ProjectComponent: RandomString, 220 | level: 1, 221 | title: "Random String Generator", 222 | description: 223 | "A simple web app that generates a random alphanumeric string with each button click.", 224 | htmlSnippet: `<!DOCTYPE html> 225 | <html lang="en"> 226 | 227 | <head> 228 | <meta charset="UTF-8"> 229 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 230 | <title>Random String Generator</title> 231 | <link rel="stylesheet" href="/styles.css"> 232 | </head> 233 | 234 | <body> 235 | <div class="container"> 236 | <div class="string-box"> 237 | <p class="random-string" id="randomString">A1b2C3</p> 238 | <button id="generateStringBtn">Random String</button> 239 | </div> 240 | </div> 241 | </body> 242 | 243 | <script src="/script.js"></script> 244 | </html>`, 245 | cssSnippet: `@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); 246 | 247 | * { 248 | margin: 0; 249 | padding: 0; 250 | box-sizing: border-box; 251 | font-family: "Inter", sans-serif; 252 | } 253 | 254 | .container { 255 | display: flex; 256 | justify-content: center; 257 | align-items: center; 258 | height: 100vh; 259 | } 260 | 261 | .string-box { 262 | padding: 20px; 263 | border-radius: 10px; 264 | text-align: center; 265 | border: 2px solid #E4E4E7; 266 | } 267 | 268 | .random-string { 269 | font-size: 1.5em; 270 | font-weight: bold; 271 | margin-bottom: 20px; 272 | } 273 | 274 | button { 275 | padding: 10px 20px; 276 | background-color: #6B7280; 277 | color: white; 278 | border: none; 279 | border-radius: 5px; 280 | cursor: pointer; 281 | } 282 | 283 | button:hover { 284 | background-color: #4b515c; 285 | }`, 286 | jsSnippet: `// Function to generate a random string of specified length 287 | function generateRandomString(length) { 288 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 289 | let result = ''; 290 | for (let i = 0; i < length; i++) { 291 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 292 | } 293 | return result; 294 | } 295 | 296 | // Function to change the displayed random string 297 | function changeString() { 298 | const randomStringElement = document.getElementById('randomString'); 299 | const newRandomString = generateRandomString(6); // Generates a string of length 6 300 | randomStringElement.textContent = newRandomString; 301 | } 302 | 303 | // Add event listener to button 304 | document.getElementById('generateStringBtn').addEventListener('click', changeString);`, 305 | 306 | projectStars: 0, 307 | downloadLink: 308 | "#", 309 | }, 310 | 311 | { 312 | name: "Gradient Generator", 313 | path: "/apps/gradient-generator", 314 | icon: "/icons/randomgradient.svg", 315 | ProjectComponent: RandomGradient, 316 | level: 1, 317 | title: "Gradient Generator", 318 | description: 319 | "A simple web app that generates a Random Gradient at the click of a button, making it easy to visualize dynamic color schemes.", 320 | htmlSnippet: `<!DOCTYPE html> 321 | <html lang="en"> 322 | 323 | <head> 324 | <meta charset="UTF-8"> 325 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 326 | <title>Background Gradient Generator</title> 327 | <link rel="stylesheet" href="/styles.css"> 328 | </head> 329 | 330 | <body> 331 | <div class="container"> 332 | <div class="card"> 333 | <div class="gradient-box" id="gradientBox"></div> 334 | <button id="generateButton" class="btn">Generate Gradient</button> 335 | </div> 336 | </div> 337 | </body> 338 | <script src="/script.js"></script> 339 | 340 | </html>`, 341 | cssSnippet: `@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); 342 | 343 | * { 344 | box-sizing: border-box; 345 | margin: 0; 346 | padding: 0; 347 | font-family: "Inter", sans-serif; 348 | } 349 | 350 | .container { 351 | display: flex; 352 | justify-content: center; 353 | align-items: center; 354 | height: 100vh; 355 | } 356 | 357 | .card { 358 | padding: 20px; 359 | border-radius: 10px; 360 | border: 2px solid #E4E4E7; 361 | text-align: center; 362 | } 363 | 364 | .gradient-box { 365 | width: 300px; 366 | height: 150px; 367 | margin-bottom: 20px; 368 | border-radius: 10px; 369 | background: linear-gradient(to right, cyan, blue); 370 | } 371 | 372 | .btn { 373 | background-color: #6b7280; 374 | color: white; 375 | padding: 10px 20px; 376 | border: none; 377 | border-radius: 5px; 378 | cursor: pointer; 379 | width: 100%; 380 | } 381 | 382 | .btn:hover { 383 | background-color: #4b515c; 384 | }`, 385 | jsSnippet: `// Function to generate a random hex color 386 | function getRandomColor() { 387 | const letters = "0123456789ABCDEF"; 388 | let color = "#"; 389 | for (let i = 0; i < 6; i++) { 390 | color += letters[Math.floor(Math.random() * 16)]; 391 | } 392 | return color; 393 | } 394 | 395 | // Function to generate a random gradient 396 | function generateRandomGradient() { 397 | const color1 = getRandomColor(); 398 | const color2 = getRandomColor(); 399 | const angle = Math.floor(Math.random() * 360); // Random angle between 0 and 360 400 | const randomGradient = `linear-gradient(${angle}deg, ${color1}, ${color2})`; 401 | document.getElementById("gradientBox").style.background = randomGradient; 402 | } 403 | 404 | // Event listener for the button click 405 | document.getElementById("generateButton").addEventListener("click", generateRandomGradient);`, 406 | projectStars: 0, 407 | downloadLink: 408 | "https://github.com/hernandoabella/random-gradient/archive/refs/heads/main.zip", 409 | }, 410 | 411 | { 412 | name: "Toggle Dark Mode", 413 | path: "/apps/toggle-dark-mode", 414 | icon: "/icons/themeswitcher.svg", 415 | ProjectComponent: ThemeSwitcher, 416 | level: 2, 417 | title: "Theme Switcher", 418 | description: "This app allow users to toggle between light and dark themes.", 419 | htmlSnippet: ``, 420 | cssSnippet: ``, 421 | jsSnippet: ``, 422 | projectStars: 0, 423 | downloadLink: "https://github.com/hernandoabella/theme-switcher/archive/refs/heads/main.zip", 424 | }, 425 | 426 | { 427 | name: "Music Player", 428 | path: "/apps/music-player", 429 | icon: "icons/musicplayer.svg", 430 | ProjectComponent: RandomGradient, 431 | level: 3, 432 | title: "Background Gradient Generator", 433 | description: "This app generates a random number.", 434 | htmlSnippet: `
Hllo, Random Number!
`, 435 | cssSnippet: `div { color: red; }`, 436 | jsSnippet: `console.log('Hello, World!');`, 437 | projectStars: 0, 438 | downloadLink: "", 439 | }, 440 | 441 | { 442 | name: "Tic Tac Toe", 443 | path: "/apps/tic-tac-toe", 444 | icon: "icons/tictactoe.svg", 445 | ProjectComponent: RandomGradient, 446 | level: 4, 447 | title: "Background Gradient Generator", 448 | description: "This app generates a random number.", 449 | htmlSnippet: `
Hllo, Random Number!
`, 450 | cssSnippet: `div { color: red; }`, 451 | jsSnippet: `console.log('Hello, World!');`, 452 | projectStars: 0, 453 | downloadLink: "", 454 | }, 455 | 456 | { 457 | name: "Tetris Game", 458 | path: "/apps/tetris-game", 459 | icon: "icons/tetris.svg", 460 | ProjectComponent: RandomGradient, 461 | level: 5, 462 | title: "Background Gradient Generator", 463 | description: "This app generates a random number.", 464 | htmlSnippet: `
Hllo, Random Number!
`, 465 | cssSnippet: `div { color: red; }`, 466 | jsSnippet: `console.log('Hello, World!');`, 467 | projectStars: 0, 468 | downloadLink: "", 469 | }, 470 | ]; 471 | -------------------------------------------------------------------------------- /app/apps/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | // app/apps/[id]/page.tsx 2 | import { apps } from "@/app/appData"; // Adjust the path as necessary 3 | import ProjectDetail from "@/app/components/ProjectDetail"; // Adjust the path as necessary 4 | import { notFound } from "next/navigation"; 5 | 6 | interface Params { 7 | params: { 8 | id: string; 9 | }; 10 | } 11 | 12 | const AppPage = async ({ params }: Params) => { 13 | const app = apps.find((a) => a.path.split('/').pop() === params.id); 14 | 15 | // If the app is not found, show a 404 page 16 | if (!app) { 17 | return notFound(); 18 | } 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }; 26 | 27 | export default AppPage; 28 | -------------------------------------------------------------------------------- /app/components/About.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | const About = () => { 5 | return ( 6 |
7 |
8 |

9 | About us 10 |

11 |

12 | Challenge your skills as a JavaScript developer 13 |

14 |

15 | We provide JavaScript projects so you can challenge yourself and 16 | become a better developer. 17 |

18 |
19 |
20 |
21 | learn-by-doing 28 |

Learn by Doing

29 |

30 | Build many small and medium projects to level up. 31 |

32 |
33 |
34 | Icono 2 41 |

42 | Suggested Projects 43 |

44 |

45 | Select one of the suggested projects here to learn something new or 46 | reuse techniques already learned. 47 |

48 |
49 |
50 | Icono 3 57 |

58 | Challenge Your Skills 59 |

60 |

61 | We challenge you to practice JavaScript syntax by project creation. 62 |

63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default About; 70 | -------------------------------------------------------------------------------- /app/components/Apps.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { FaStar } from "react-icons/fa"; 4 | import { apps, App } from "@/app/appData"; 5 | 6 | const levelIcons: { [key: number]: string } = { 7 | 1: "/icons/rookie.svg", 8 | 2: "/icons/novice.svg", 9 | 3: "/icons/pro.svg", 10 | 4: "/icons/master.svg", 11 | 5: "/icons/ninja.svg", 12 | }; 13 | 14 | const Stars: React.FC<{ level: number }> = ({ level }) => { 15 | const stars = Array.from({ length: level }, (_, i) => ( 16 | 17 | )); 18 | return {stars}; 19 | }; 20 | 21 | const levelDescriptions: { [key: number]: string } = { 22 | 1: "Rookie: Just starting.", 23 | 2: "Novice: Gaining confidence.", 24 | 3: "Pro: Building real-world apps.", 25 | 4: "Master: Leading projects.", 26 | 5: "Ninja: Tech innovator.", 27 | }; 28 | 29 | const Apps: React.FC = () => { 30 | const appsByLevel: { [key: number]: App[] } = apps.reduce((acc, app) => { 31 | if (!acc[app.level]) { 32 | acc[app.level] = []; 33 | } 34 | acc[app.level].push(app); 35 | return acc; 36 | }, {} as { [key: number]: App[] }); 37 | 38 | return ( 39 |
40 | {Object.keys(appsByLevel).map((level) => ( 41 |
42 | 43 |
44 |
47 |
49 |
50 |

51 | {levelDescriptions[parseInt(level, 56 |
57 | 58 | {levelDescriptions[parseInt(level, 10)].split(":")[0]}: 59 | 60 | 61 |
62 |

63 |

64 | {levelDescriptions[parseInt(level, 10)].split(":")[1]} 65 |

66 |
67 | {appsByLevel[parseInt(level, 10)].map((app, index) => ( 68 |
73 | 74 | 75 | 91 | 92 | 93 |
94 | ))} 95 |
96 |
97 |
98 |
99 | 100 | 101 |
102 | ))} 103 |
104 | ); 105 | }; 106 | 107 | export default Apps; 108 | -------------------------------------------------------------------------------- /app/components/Cta.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Cta = () => { 4 | return ( 5 |
6 |

7 | Ready to improve your programming skills? 8 |

9 |

10 | Discover our apps to take your skills to the next level. 11 |

12 |
13 | 14 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Cta; 26 | -------------------------------------------------------------------------------- /app/components/Faq.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Faq = () => { 4 | return ( 5 |
Faq
6 | ) 7 | } 8 | 9 | export default Faq -------------------------------------------------------------------------------- /app/components/Featured.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Featured = () => { 4 | return ( 5 |
Featured
6 | ) 7 | } 8 | 9 | export default Featured -------------------------------------------------------------------------------- /app/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 |

7 | Made with ❤️ by Hernando Abella 8 |

9 |
10 | ); 11 | }; 12 | 13 | export default Footer; 14 | -------------------------------------------------------------------------------- /app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | import { FaGithub } from "react-icons/fa"; 4 | import { ModeToggle } from "./ModeToggle"; 5 | 6 | const Header = () => { 7 | return ( 8 |
9 | 91 |
92 | ); 93 | }; 94 | 95 | export default Header; 96 | -------------------------------------------------------------------------------- /app/components/ModeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/components/ProjectComponent.tsx: -------------------------------------------------------------------------------- 1 | // ProjectComponent.tsx 2 | import React from "react"; 3 | 4 | interface ProjectComponentProps { 5 | name: string; 6 | description: string; 7 | path: string; 8 | } 9 | 10 | const ProjectComponent: React.FC = ({ 11 | name, 12 | description, 13 | path, 14 | }) => { 15 | return ( 16 |
17 |

{name}

18 |

{description}

19 | 23 | View Project 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default ProjectComponent; 30 | -------------------------------------------------------------------------------- /app/components/ProjectDetail.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import "highlight.js/styles/github-dark.css"; 3 | import { useState, useEffect, useRef } from "react"; 4 | import { 5 | FaHtml5, 6 | FaCss3, 7 | FaArrowCircleLeft, 8 | FaJs, 9 | FaCopy, 10 | FaDownload, 11 | FaCheck, 12 | } from "react-icons/fa"; 13 | 14 | import hljs from "highlight.js/lib/core"; 15 | import html from "highlight.js/lib/languages/xml"; // Import HTML language 16 | import css from "highlight.js/lib/languages/css"; // Import CSS language 17 | import javascript from "highlight.js/lib/languages/javascript"; // Import JavaScript language 18 | import { App } from "@/app/appData"; 19 | import Header from "./Header"; 20 | import Footer from "./Footer"; 21 | 22 | // Register languages with highlight.js 23 | hljs.registerLanguage("html", html); 24 | hljs.registerLanguage("css", css); 25 | hljs.registerLanguage("javascript", javascript); 26 | 27 | interface ProjectDetailProps { 28 | app: App; // Use the updated App interface 29 | } 30 | 31 | const ProjectDetail = ({ app }: ProjectDetailProps) => { 32 | const [codeType, setCodeType] = useState<"html" | "css" | "js">("html"); 33 | const codeRef = useRef(null); 34 | const [copied, setCopied] = useState(false); // Estado para manejar el ícono de copia 35 | 36 | // Snippets for code highlighting 37 | const codeSnippets = { 38 | html: app.htmlSnippet 39 | ? `
${app.htmlSnippet}
` 40 | : "", 41 | css: app.cssSnippet 42 | ? `
${app.cssSnippet}
` 43 | : "", 44 | js: app.jsSnippet 45 | ? `
${app.jsSnippet}
` 46 | : "", 47 | }; 48 | 49 | useEffect(() => { 50 | if (codeRef.current) { 51 | codeRef.current.innerHTML = codeSnippets[codeType]; 52 | codeRef.current.querySelectorAll("pre code").forEach((block) => { 53 | if (block instanceof HTMLElement) { 54 | hljs.highlightElement(block); 55 | } 56 | }); 57 | } 58 | }, [codeType]); 59 | 60 | const handleCopyToClipboard = () => { 61 | if (navigator.clipboard) { 62 | navigator.clipboard 63 | .writeText(codeRef.current?.innerText || "") 64 | .then(() => { 65 | setCopied(true); // Cambia el estado a true cuando se copia 66 | setTimeout(() => setCopied(false), 2000); // Restablece el estado después de 2 segundos 67 | }) 68 | .catch((err) => { 69 | console.error("Could not copy text: ", err); 70 | }); 71 | } else { 72 | const textarea = document.createElement("textarea"); 73 | textarea.value = codeRef.current?.innerText || ""; 74 | textarea.setAttribute("readonly", ""); 75 | textarea.style.position = "absolute"; 76 | textarea.style.left = "-9999px"; 77 | document.body.appendChild(textarea); 78 | textarea.select(); 79 | document.execCommand("copy"); 80 | document.body.removeChild(textarea); 81 | setCopied(true); // Cambia el estado a true cuando se copia 82 | setTimeout(() => setCopied(false), 2000); // Restablece el estado después de 2 segundos 83 | } 84 | }; 85 | 86 | return ( 87 |
88 | 89 |
90 |
91 |
92 | 93 |
94 |
97 |
100 |
101 | 105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 | 113 |
114 | 115 |
116 |
117 | {app.ProjectComponent ? : null} 118 |
119 |
120 | 121 |
122 |
125 |
128 |
129 | {/* HTML Button */} 130 | 139 | 140 | {/* CSS Button */} 141 | 150 | 151 | {/* JS Button */} 152 | 161 |
162 |
163 |
164 | 171 |
172 |
173 |
174 | 175 | 176 |
177 |
178 | 179 | 180 |
181 |
182 | 183 |
184 |
187 |
190 |

{app.title}

191 |

{app.description}

192 |
193 |
194 | 195 |
196 |
197 | 198 | {/* */} 207 |
{" "} 208 |
209 |
210 |
211 |
212 | 213 |
214 | ); 215 | }; 216 | 217 | export default ProjectDetail; 218 | -------------------------------------------------------------------------------- /app/components/projects/ColorGenerator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | 5 | // Function to generate a random color in hex format 6 | const generateRandomColor = () => { 7 | const letters = "0123456789ABCDEF"; 8 | let color = "#"; 9 | for (let i = 0; i < 6; i++) { 10 | color += letters[Math.floor(Math.random() * 16)]; 11 | } 12 | return color; 13 | }; 14 | 15 | // Function to determine if a color is light or dark for contrast 16 | const isLightColor = (color: string) => { 17 | const r = parseInt(color.slice(1, 3), 16); 18 | const g = parseInt(color.slice(3, 5), 16); 19 | const b = parseInt(color.slice(5, 7), 16); 20 | // Use the luminance formula to determine brightness 21 | const luminance = 0.299 * r + 0.587 * g + 0.114 * b; 22 | return luminance > 186; // If luminance > 186, it's a light color 23 | }; 24 | 25 | const ColorGenerator = () => { 26 | // State to store the random color 27 | const [randomColor, setRandomColor] = useState("#FF5733"); 28 | 29 | // Function to generate a random color when the button is clicked 30 | const generateColor = () => { 31 | const newRandomColor = generateRandomColor(); 32 | setRandomColor(newRandomColor); 33 | }; 34 | 35 | // Determine text color based on the background color 36 | const textColor = isLightColor(randomColor) ? "text-black" : "text-white"; 37 | 38 | return ( 39 |
40 |
41 |

45 | {randomColor} 46 |

47 | 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default ColorGenerator; 59 | -------------------------------------------------------------------------------- /app/components/projects/GradientGenerator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | 5 | const GradientGenerator = () => { 6 | // State to store the random gradient CSS value 7 | const [gradient, setGradient] = useState( 8 | "linear-gradient(to right, cyan, blue)" 9 | ); 10 | 11 | // Function to generate a random hex color 12 | const getRandomColor = () => { 13 | const letters = "0123456789ABCDEF"; 14 | let color = "#"; 15 | for (let i = 0; i < 6; i++) { 16 | color += letters[Math.floor(Math.random() * 16)]; 17 | } 18 | return color; 19 | }; 20 | 21 | // Function to generate a random gradient 22 | const generateRandomGradient = () => { 23 | const color1 = getRandomColor(); 24 | const color2 = getRandomColor(); 25 | const angle = Math.floor(Math.random() * 360); // Random angle between 0 and 360 26 | const randomGradient = `linear-gradient(${angle}deg, ${color1}, ${color2})`; 27 | setGradient(randomGradient); 28 | }; 29 | 30 | return ( 31 |
32 |
33 |
34 |
38 | 39 |
40 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default GradientGenerator; 53 | -------------------------------------------------------------------------------- /app/components/projects/NumberGenerator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | 5 | const NumberGenerator = () => { 6 | // State to store the random number 7 | const [randomNumber, setRandomNumber] = useState(7); 8 | 9 | // Function to generate a random number 10 | const generateRandomNumber = () => { 11 | const newRandomNumber = Math.floor(Math.random() * 100) + 1; 12 | setRandomNumber(newRandomNumber); 13 | }; 14 | 15 | return ( 16 |
17 |
18 |

19 | {randomNumber} 20 |

21 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default NumberGenerator; 33 | -------------------------------------------------------------------------------- /app/components/projects/StringGenerator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | 5 | // Function to generate a random string 6 | const generateRandomString = (length: number) => { 7 | const characters = 8 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 9 | let result = ""; 10 | for (let i = 0; i < length; i++) { 11 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 12 | } 13 | return result; 14 | }; 15 | 16 | const RandomString = () => { 17 | // State to store the random string 18 | const [randomString, setRandomString] = useState("A1b2C3"); 19 | 20 | // Function to generate a random string when button is clicked 21 | const generateString = () => { 22 | const newRandomString = generateRandomString(6); // Generates a random string of length 6 23 | setRandomString(newRandomString); 24 | }; 25 | 26 | return ( 27 |
28 |
29 |

30 | {randomString} 31 |

32 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default RandomString; 44 | -------------------------------------------------------------------------------- /app/components/projects/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | const ThemeSwitcher = () => { 6 | const [theme, setTheme] = useState(() => { 7 | if (typeof window !== "undefined") { 8 | return localStorage.getItem("theme") || "light"; 9 | } 10 | return "light"; 11 | }); 12 | 13 | // Store the current theme in localStorage on change 14 | useEffect(() => { 15 | localStorage.setItem("theme", theme); 16 | }, [theme]); 17 | 18 | const toggleTheme = () => { 19 | setTheme(theme === "light" ? "dark" : "light"); 20 | }; 21 | 22 | return ( 23 |
30 |
35 |

36 | {theme === "light" ? "Light Mode" : "Dark Mode"} 37 |

38 | 39 |
40 | 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default ThemeSwitcher; -------------------------------------------------------------------------------- /app/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes" 4 | import { type ThemeProviderProps } from "next-themes/dist/types" 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children} 8 | } 9 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hernandoabella/js-apps/0dd641daa11b2095f26d4f5ab9665959605c70ed/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 255, 255, 255; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | @layer utilities { 20 | .text-balance { 21 | text-wrap: balance; 22 | } 23 | } 24 | 25 | /* From Uiverse.io by milegelu */ 26 | .button { 27 | --bezier: cubic-bezier(0.22, 0.61, 0.36, 1); 28 | --edge-light: hsla(0, 0%, 50%, 0.8); 29 | --text-light: rgba(255, 255, 255, 0.4); 30 | --back-color: 240, 40%; 31 | 32 | cursor: pointer; 33 | padding: 0.7em 1em; 34 | border-radius: 0.5em; 35 | min-height: 2.4em; 36 | min-width: 3em; 37 | display: flex; 38 | align-items: center; 39 | gap: 0.5em; 40 | 41 | font-size: 18px; 42 | letter-spacing: 0.05em; 43 | line-height: 1; 44 | font-weight: bold; 45 | 46 | background: linear-gradient(140deg, 47 | hsla(var(--back-color), 50%, 1) min(2em, 20%), 48 | hsla(var(--back-color), 50%, 0.6) min(8em, 100%)); 49 | color: hsla(0, 0%, 90%); 50 | border: 0; 51 | box-shadow: inset 0.4px 1px 4px var(--edge-light); 52 | 53 | transition: all 0.1s var(--bezier); 54 | } 55 | 56 | .button:hover { 57 | --edge-light: hsla(0, 0%, 50%, 1); 58 | text-shadow: 0px 0px 10px var(--text-light); 59 | box-shadow: inset 0.4px 1px 4px var(--edge-light), 60 | 2px 4px 8px hsla(0, 0%, 0%, 0.295); 61 | transform: scale(1.1); 62 | } 63 | 64 | .button:active { 65 | --text-light: rgba(255, 255, 255, 1); 66 | 67 | background: linear-gradient(140deg, 68 | hsla(var(--back-color), 50%, 1) min(2em, 20%), 69 | hsla(var(--back-color), 50%, 0.6) min(8em, 100%)); 70 | box-shadow: inset 0.4px 1px 8px var(--edge-light), 71 | 0px 0px 8px hsla(var(--back-color), 50%, 0.6); 72 | text-shadow: 0px 0px 20px var(--text-light); 73 | color: hsla(0, 0%, 100%, 1); 74 | letter-spacing: 0.1em; 75 | transform: scale(1); 76 | } 77 | 78 | 79 | @layer base { 80 | :root { 81 | --background: 0 0% 100%; 82 | --foreground: 240 10% 3.9%; 83 | --card: 0 0% 100%; 84 | --card-foreground: 240 10% 3.9%; 85 | --popover: 0 0% 100%; 86 | --popover-foreground: 240 10% 3.9%; 87 | --primary: 240 5.9% 10%; 88 | --primary-foreground: 0 0% 98%; 89 | --secondary: 240 4.8% 95.9%; 90 | --secondary-foreground: 240 5.9% 10%; 91 | --muted: 240 4.8% 95.9%; 92 | --muted-foreground: 240 3.8% 46.1%; 93 | --accent: 240 4.8% 95.9%; 94 | --accent-foreground: 240 5.9% 10%; 95 | --destructive: 0 84.2% 60.2%; 96 | --destructive-foreground: 0 0% 98%; 97 | --border: 240 5.9% 90%; 98 | --input: 240 5.9% 90%; 99 | --ring: 240 10% 3.9%; 100 | --chart-1: 12 76% 61%; 101 | --chart-2: 173 58% 39%; 102 | --chart-3: 197 37% 24%; 103 | --chart-4: 43 74% 66%; 104 | --chart-5: 27 87% 67%; 105 | --radius: 0.5rem; 106 | } 107 | 108 | .dark { 109 | --background: 145%, 120%, 20%; 110 | --foreground: 0 0% 98%; 111 | --card: 240 10% 3.9%; 112 | --card-foreground: 0 0% 98%; 113 | --popover: 240 10% 3.9%; 114 | --popover-foreground: 0 0% 98%; 115 | --primary: 0 0% 98%; 116 | --primary-foreground: 240 5.9% 10%; 117 | --secondary: 240 3.7% 15.9%; 118 | --secondary-foreground: 0 0% 98%; 119 | --muted: 240 3.7% 15.9%; 120 | --muted-foreground: 240 5% 64.9%; 121 | --accent: 240 3.7% 15.9%; 122 | --accent-foreground: 0 0% 98%; 123 | --destructive: 0 62.8% 30.6%; 124 | --destructive-foreground: 0 0% 98%; 125 | --border: 240 3.7% 15.9%; 126 | --input: 240 3.7% 15.9%; 127 | --ring: 240 4.9% 83.9%; 128 | --chart-1: 220 70% 50%; 129 | --chart-2: 160 60% 45%; 130 | --chart-3: 30 80% 55%; 131 | --chart-4: 280 65% 60%; 132 | --chart-5: 340 75% 55%; 133 | } 134 | } 135 | 136 | @layer base { 137 | * { 138 | @apply border-border; 139 | } 140 | 141 | body { 142 | @apply bg-background text-foreground; 143 | } 144 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { Squada_One } from "next/font/google"; 4 | import "./globals.css"; 5 | import { ThemeProvider } from "./components/theme-provider"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | const squada = Squada_One({ 10 | subsets: ['latin'], 11 | weight: '400', 12 | variable: '--font-squada', 13 | }); 14 | 15 | 16 | export const metadata: Metadata = { 17 | title: ") { 26 | return ( 27 | 28 | 29 | 35 | {children} 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/app/components/Header"; 2 | import Apps from "@/app/components/Apps"; 3 | import Footer from "@/app/components/Footer"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons" 10 | 11 | import { cn } from "@/lib/utils" 12 | 13 | const DropdownMenu = DropdownMenuPrimitive.Root 14 | 15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 16 | 17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 18 | 19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 20 | 21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 22 | 23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 24 | 25 | const DropdownMenuSubTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | inset?: boolean 29 | } 30 | >(({ className, inset, children, ...props }, ref) => ( 31 | 40 | {children} 41 | 42 | 43 | )) 44 | DropdownMenuSubTrigger.displayName = 45 | DropdownMenuPrimitive.SubTrigger.displayName 46 | 47 | const DropdownMenuSubContent = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, ...props }, ref) => ( 51 | 59 | )) 60 | DropdownMenuSubContent.displayName = 61 | DropdownMenuPrimitive.SubContent.displayName 62 | 63 | const DropdownMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, sideOffset = 4, ...props }, ref) => ( 67 | 68 | 78 | 79 | )) 80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 81 | 82 | const DropdownMenuItem = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef & { 85 | inset?: boolean 86 | } 87 | >(({ className, inset, ...props }, ref) => ( 88 | 97 | )) 98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 99 | 100 | const DropdownMenuCheckboxItem = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, children, checked, ...props }, ref) => ( 104 | 113 | 114 | 115 | 116 | 117 | 118 | {children} 119 | 120 | )) 121 | DropdownMenuCheckboxItem.displayName = 122 | DropdownMenuPrimitive.CheckboxItem.displayName 123 | 124 | const DropdownMenuRadioItem = React.forwardRef< 125 | React.ElementRef, 126 | React.ComponentPropsWithoutRef 127 | >(({ className, children, ...props }, ref) => ( 128 | 136 | 137 | 138 | 139 | 140 | 141 | {children} 142 | 143 | )) 144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 145 | 146 | const DropdownMenuLabel = React.forwardRef< 147 | React.ElementRef, 148 | React.ComponentPropsWithoutRef & { 149 | inset?: boolean 150 | } 151 | >(({ className, inset, ...props }, ref) => ( 152 | 161 | )) 162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 163 | 164 | const DropdownMenuSeparator = React.forwardRef< 165 | React.ElementRef, 166 | React.ComponentPropsWithoutRef 167 | >(({ className, ...props }, ref) => ( 168 | 173 | )) 174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 175 | 176 | const DropdownMenuShortcut = ({ 177 | className, 178 | ...props 179 | }: React.HTMLAttributes) => { 180 | return ( 181 | 185 | ) 186 | } 187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 188 | 189 | export { 190 | DropdownMenu, 191 | DropdownMenuTrigger, 192 | DropdownMenuContent, 193 | DropdownMenuItem, 194 | DropdownMenuCheckboxItem, 195 | DropdownMenuRadioItem, 196 | DropdownMenuLabel, 197 | DropdownMenuSeparator, 198 | DropdownMenuShortcut, 199 | DropdownMenuGroup, 200 | DropdownMenuPortal, 201 | DropdownMenuSub, 202 | DropdownMenuSubContent, 203 | DropdownMenuSubTrigger, 204 | DropdownMenuRadioGroup, 205 | } 206 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-apps", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-avatar": "^1.1.1", 13 | "@radix-ui/react-dropdown-menu": "^2.1.2", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "@types/highlight.js": "^10.1.0", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "highlight.js": "^11.9.0", 20 | "lucide-react": "^0.453.0", 21 | "next": "^14.2.16", 22 | "next-themes": "^0.3.0", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "react-icons": "^5.2.1", 26 | "tailwind-merge": "^2.5.4", 27 | "tailwindcss-animate": "^1.0.7" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^20", 31 | "@types/react": "^18", 32 | "@types/react-dom": "^18", 33 | "eslint": "^8", 34 | "eslint-config-next": "14.2.4", 35 | "postcss": "^8", 36 | "tailwindcss": "^3.4.1", 37 | "typescript": "^5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hernandoabella/js-apps/0dd641daa11b2095f26d4f5ab9665959605c70ed/public/avatar.png -------------------------------------------------------------------------------- /public/icons/master.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 19 | 20 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 35 | 36 | 39 | 40 | 42 | 45 | 46 | 47 | 49 | 51 | 53 | 54 | 57 | 59 | 60 | 62 | 63 | 65 | 66 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /public/icons/musicplayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/ninja.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 30 | 31 | 32 | 33 | 35 | 36 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 91 | 94 | 97 | 99 | 101 | 103 | 104 | 105 | 107 | 109 | 110 | 111 | 113 | 115 | 117 | 119 | 120 | 121 | 123 | 125 | 127 | 129 | 131 | 133 | 135 | 137 | 138 | -------------------------------------------------------------------------------- /public/icons/novice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 13 | 15 | 16 | 17 | 18 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 38 | 40 | 41 | 42 | 46 | 48 | 49 | 53 | 54 | 56 | 59 | 60 | 61 | 63 | 64 | 66 | 69 | 71 | 73 | 76 | 78 | 79 | 81 | 83 | 84 | 85 | 87 | 89 | -------------------------------------------------------------------------------- /public/icons/pro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 13 | 14 | 17 | 19 | 20 | 21 | 22 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/icons/randomcolor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/randomgradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/randomnumber.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/randomstring.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/rookie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 21 | 23 | 25 | 28 | 29 | 31 | 34 | 35 | 37 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 48 | 50 | 52 | 54 | 55 | 58 | 60 | 61 | 62 | 64 | 66 | 67 | -------------------------------------------------------------------------------- /public/icons/tetris.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/themeswitcher.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/tictactoe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hernandoabella/js-apps/0dd641daa11b2095f26d4f5ab9665959605c70ed/public/logo.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | backgroundImage: { 13 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 14 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' 15 | }, 16 | borderRadius: { 17 | lg: 'var(--radius)', 18 | md: 'calc(var(--radius) - 2px)', 19 | sm: 'calc(var(--radius) - 4px)' 20 | }, 21 | colors: { 22 | background: 'hsl(var(--background))', 23 | foreground: 'hsl(var(--foreground))', 24 | card: { 25 | DEFAULT: 'hsl(var(--card))', 26 | foreground: 'hsl(var(--card-foreground))' 27 | }, 28 | popover: { 29 | DEFAULT: 'hsl(var(--popover))', 30 | foreground: 'hsl(var(--popover-foreground))' 31 | }, 32 | primary: { 33 | DEFAULT: 'hsl(var(--primary))', 34 | foreground: 'hsl(var(--primary-foreground))' 35 | }, 36 | secondary: { 37 | DEFAULT: 'hsl(var(--secondary))', 38 | foreground: 'hsl(var(--secondary-foreground))' 39 | }, 40 | muted: { 41 | DEFAULT: 'hsl(var(--muted))', 42 | foreground: 'hsl(var(--muted-foreground))' 43 | }, 44 | accent: { 45 | DEFAULT: 'hsl(var(--accent))', 46 | foreground: 'hsl(var(--accent-foreground))' 47 | }, 48 | destructive: { 49 | DEFAULT: 'hsl(var(--destructive))', 50 | foreground: 'hsl(var(--destructive-foreground))' 51 | }, 52 | border: 'hsl(var(--border))', 53 | input: 'hsl(var(--input))', 54 | ring: 'hsl(var(--ring))', 55 | chart: { 56 | '1': 'hsl(var(--chart-1))', 57 | '2': 'hsl(var(--chart-2))', 58 | '3': 'hsl(var(--chart-3))', 59 | '4': 'hsl(var(--chart-4))', 60 | '5': 'hsl(var(--chart-5))' 61 | } 62 | } 63 | } 64 | }, 65 | plugins: [require("tailwindcss-animate")], 66 | }; 67 | export default config; 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------