├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── hangman-preview.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── site.webmanifest ├── src ├── assets │ ├── hangman-1.png │ ├── hangman-2.png │ ├── hangman-3.png │ ├── hangman-4.png │ ├── hangman-5.png │ ├── hangman-6.png │ └── hangman-7.png ├── main.jsx ├── components │ ├── Letter.jsx │ ├── Footer.jsx │ ├── Word.jsx │ ├── Hangman.jsx │ ├── Keyboard.jsx │ └── ui │ │ └── Layout.jsx ├── index.css ├── App.jsx └── word-list.json ├── screenshots ├── screenshot-1.png └── screenshot-2.png ├── postcss.config.js ├── vite.config.js ├── tailwind.config.js ├── .gitignore ├── .eslintrc.cjs ├── package.json ├── README.md ├── index.html └── pnpm-lock.yaml /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/hangman-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-1.png -------------------------------------------------------------------------------- /src/assets/hangman-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-2.png -------------------------------------------------------------------------------- /src/assets/hangman-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-3.png -------------------------------------------------------------------------------- /src/assets/hangman-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-4.png -------------------------------------------------------------------------------- /src/assets/hangman-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-5.png -------------------------------------------------------------------------------- /src/assets/hangman-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-6.png -------------------------------------------------------------------------------- /src/assets/hangman-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/src/assets/hangman-7.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/hangman-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/hangman-preview.png -------------------------------------------------------------------------------- /screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /screenshots/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/screenshots/screenshot-2.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/hangman/main/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | }; 9 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/Letter.jsx: -------------------------------------------------------------------------------- 1 | const Letter = ({ guessedLetters, letter, reveal }) => { 2 | return ( 3 | <> 4 | {(guessedLetters.includes(letter) || reveal) && ( 5 |
{letter}
6 | )} 7 | 8 | ); 9 | }; 10 | 11 | export default Letter; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 | 16 | ); 17 | }; 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended' 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true } 18 | ], 19 | 'react/prop-types': 'off' 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Word.jsx: -------------------------------------------------------------------------------- 1 | import Letter from './Letter'; 2 | 3 | const Word = ({ guessedLetters, wordToGuess, reveal = false }) => { 4 | return ( 5 |
6 | {wordToGuess.split('').map((letter, id) => ( 7 |
11 | 16 | {letter} 17 | 18 |
19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default Word; 25 | -------------------------------------------------------------------------------- /src/components/Hangman.jsx: -------------------------------------------------------------------------------- 1 | import Hangman1 from '../assets/hangman-1.png'; 2 | import Hangman2 from '../assets/hangman-2.png'; 3 | import Hangman3 from '../assets/hangman-3.png'; 4 | import Hangman4 from '../assets/hangman-4.png'; 5 | import Hangman5 from '../assets/hangman-5.png'; 6 | import Hangman6 from '../assets/hangman-6.png'; 7 | import Hangman7 from '../assets/hangman-7.png'; 8 | 9 | const HangmanParts = [ 10 | Hangman1, 11 | Hangman2, 12 | Hangman3, 13 | Hangman4, 14 | Hangman5, 15 | Hangman6, 16 | Hangman7 17 | ]; 18 | 19 | const Hangman = ({ numberOfGuesses }) => { 20 | return ( 21 |
22 | 26 |
27 | ); 28 | }; 29 | 30 | export default Hangman; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spongebob-hangman", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "lucide-react": "^0.263.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.15", 19 | "@types/react-dom": "^18.2.7", 20 | "@vitejs/plugin-react": "^4.0.3", 21 | "autoprefixer": "^10.4.14", 22 | "eslint": "^8.45.0", 23 | "eslint-plugin-react": "^7.32.2", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.3", 26 | "postcss": "^8.4.27", 27 | "tailwindcss": "^3.3.3", 28 | "vite": "^4.4.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Hangman](https://hangman.salimi.my) · [![Author Salimi](https://img.shields.io/badge/Author-Salimi-%3C%3E)](https://www.linkedin.com/in/mohamad-salimi/) 2 | 3 | This is a classic word puzzle game Hangman for testing vocabulary by guessing letters one at a time to solve it created using React. 4 | 5 | ## Hangman 6 | 7 | - Word puzzle game 8 | - Created using React 9 | 10 | ## Tech/framework used 11 | 12 | - React 13 | - Tailwind 14 | 15 | ## Starting the project 16 | 17 | Open the project then run the following command: 18 | 19 | ```bash 20 | npm install 21 | # then 22 | npm run dev 23 | # to build 24 | npm run build 25 | ``` 26 | 27 | ## Demo 28 | 29 | Hosted privately on personal DigitalOcean Droplet. [Click here](https://hangman.salimi.my) to visit. 30 |
31 | Direct link: `https://hangman.salimi.my` 32 | 33 | ## Screenshots 34 | 35 | #### Win 36 | 37 | ![Win](/screenshots/screenshot-1.png) 38 | 39 | #### Lose 40 | 41 | ![Lose](/screenshots/screenshot-2.png) 42 | -------------------------------------------------------------------------------- /src/components/Keyboard.jsx: -------------------------------------------------------------------------------- 1 | const keys = [ 2 | 'a', 3 | 'b', 4 | 'c', 5 | 'd', 6 | 'e', 7 | 'f', 8 | 'g', 9 | 'h', 10 | 'i', 11 | 'j', 12 | 'k', 13 | 'l', 14 | 'm', 15 | 'n', 16 | 'o', 17 | 'p', 18 | 'q', 19 | 'r', 20 | 's', 21 | 't', 22 | 'u', 23 | 'v', 24 | 'w', 25 | 'x', 26 | 'y', 27 | 'z' 28 | ]; 29 | 30 | const Keyboard = ({ 31 | correctLetters, 32 | incorrectLetters, 33 | addGuessedLetter, 34 | disabled = false 35 | }) => { 36 | return ( 37 |
38 |
39 | {keys.map((key) => { 40 | const active = correctLetters.includes(key); 41 | const inActive = incorrectLetters.includes(key); 42 | 43 | return ( 44 | 54 | ); 55 | })} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default Keyboard; 62 | -------------------------------------------------------------------------------- /src/components/ui/Layout.jsx: -------------------------------------------------------------------------------- 1 | const Layout = ({ children }) => { 2 | return ( 3 | <> 4 |
5 |
{children}
6 | 7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 | ); 55 | }; 56 | 57 | export default Layout; 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Hangman — Guess The Word 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .wrapper { 6 | height: 100%; 7 | min-height: 900px; 8 | width: 100%; 9 | background: linear-gradient(180deg, #04fafd, 5%, #119dff, 70%, #030423); 10 | position: absolute; 11 | } 12 | 13 | .wrapper .bubble { 14 | height: 60px; 15 | width: 60px; 16 | border: 2px solid rgba(255, 255, 255, 0.7); 17 | border-radius: 50px; 18 | position: absolute; 19 | top: 10%; 20 | left: 10%; 21 | animation: 4s linear infinite; 22 | } 23 | 24 | .bubble .dot { 25 | height: 10px; 26 | width: 10px; 27 | border-radius: 50px; 28 | background: rgba(255, 255, 255, 0.5); 29 | position: absolute; 30 | top: 20%; 31 | right: 20%; 32 | } 33 | 34 | .wrapper .bubble:nth-child(2) { 35 | top: 20%; 36 | left: 20%; 37 | animation: animate 8s linear infinite; 38 | } 39 | 40 | .wrapper .bubble:nth-child(3) { 41 | top: 60%; 42 | left: 80%; 43 | animation: animate 10s linear infinite; 44 | } 45 | 46 | .wrapper .bubble:nth-child(4) { 47 | top: 40%; 48 | left: 40%; 49 | animation: animate 3s linear infinite; 50 | } 51 | 52 | .wrapper .bubble:nth-child(5) { 53 | top: 66%; 54 | left: 30%; 55 | animation: animate 7s linear infinite; 56 | } 57 | 58 | .wrapper .bubble:nth-child(6) { 59 | top: 90%; 60 | left: 10%; 61 | animation: animate 9s linear infinite; 62 | } 63 | 64 | .wrapper .bubble:nth-child(7) { 65 | top: 30%; 66 | left: 60%; 67 | animation: animate 5s linear infinite; 68 | } 69 | 70 | .wrapper .bubble:nth-child(8) { 71 | top: 70%; 72 | left: 20%; 73 | animation: animate 8s linear infinite; 74 | } 75 | 76 | .wrapper .bubble:nth-child(9) { 77 | top: 75%; 78 | left: 60%; 79 | animation: animate 10s linear infinite; 80 | } 81 | 82 | .wrapper .bubble:nth-child(10) { 83 | top: 50%; 84 | left: 50%; 85 | animation: animate 6s linear infinite; 86 | } 87 | 88 | .wrapper .bubble:nth-child(11) { 89 | top: 45%; 90 | left: 20%; 91 | animation: animate 10s linear infinite; 92 | } 93 | 94 | .wrapper .bubble:nth-child(12) { 95 | top: 10%; 96 | left: 90%; 97 | animation: animate 9s linear infinite; 98 | } 99 | 100 | .wrapper .bubble:nth-child(13) { 101 | top: 20%; 102 | left: 70%; 103 | animation: animate 7s linear infinite; 104 | } 105 | 106 | .wrapper .bubble:nth-child(14) { 107 | top: 20%; 108 | left: 20%; 109 | animation: animate 8s linear infinite; 110 | } 111 | 112 | .wrapper .bubble:nth-child(15) { 113 | top: 60%; 114 | left: 5%; 115 | animation: animate 6s linear infinite; 116 | } 117 | 118 | .wrapper .bubble:nth-child(16) { 119 | top: 90%; 120 | left: 80%; 121 | animation: animate 9s linear infinite; 122 | } 123 | 124 | .wrapper .bubble:nth-child(17) { 125 | top: 90%; 126 | left: 80%; 127 | animation: animate 9s linear infinite; 128 | } 129 | 130 | @keyframes animate { 131 | 0% { 132 | transform: scale(0) translateY(0) rotate(70deg); 133 | } 134 | 100% { 135 | transform: scale(1.3) translateY(-100px) rotate(360deg); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { RotateCcw } from 'lucide-react'; 3 | 4 | import words from './word-list.json'; 5 | import Hangman from './components/Hangman'; 6 | import Layout from './components/ui/Layout'; 7 | import Word from './components/Word'; 8 | import Keyboard from './components/Keyboard'; 9 | import Footer from './components/Footer'; 10 | 11 | const getRandomWord = (words) => { 12 | return words[Math.floor(Math.random() * words.length)]; 13 | }; 14 | 15 | const App = () => { 16 | const [wordToGuess, setWordToGuess] = useState(getRandomWord(words)); 17 | const [guessedLetters, setGuessedLetters] = useState([]); 18 | 19 | const incorrectLetters = guessedLetters.filter( 20 | (letter) => !wordToGuess.includes(letter) 21 | ); 22 | 23 | const isLose = incorrectLetters.length >= 6; 24 | const isWin = wordToGuess 25 | .split('') 26 | .every((letter) => guessedLetters.includes(letter)); 27 | 28 | const addGuessedLetter = (letter) => { 29 | if (!guessedLetters.includes(letter)) { 30 | setGuessedLetters((currentLetters) => [...currentLetters, letter]); 31 | } 32 | }; 33 | 34 | const restartGame = () => { 35 | setWordToGuess(getRandomWord(words)); 36 | setGuessedLetters([]); 37 | }; 38 | 39 | return ( 40 | <> 41 | 42 |
43 |

44 | Hangman 45 |

46 | 47 | {isWin && ( 48 |
49 |

You Win!

50 |
51 | )} 52 | {!isWin && } 53 | {isLose && ( 54 |
55 |

You Lose!

56 |
57 | )} 58 | {!isLose &&
} 59 | 60 | 65 | 66 | {(isWin || isLose) && ( 67 |
68 | 74 |
75 | )} 76 | 77 | {!(isWin || isLose) &&
} 78 | 79 | 82 | wordToGuess.includes(letter) 83 | )} 84 | incorrectLetters={incorrectLetters} 85 | addGuessedLetter={addGuessedLetter} 86 | /> 87 |
88 |