├── .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 | 
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 |
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 |
28 |
Learn by Doing
29 |
30 | Build many small and medium projects to level up.
31 |
32 |
33 |
34 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/public/icons/musicplayer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/ninja.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/novice.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/pro.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/randomcolor.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/randomgradient.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/randomnumber.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/randomstring.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/rookie.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/tetris.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/themeswitcher.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/icons/tictactoe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------