diff --git a/.all-contributorsrc b/.all-contributorsrc index 79d909c01..e8db90814 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -652,6 +652,15 @@ "contributions": [ "code" ] + }, + { + "login": "freemrl", + "name": "Jannik Schmidtke", + "avatar_url": "https://avatars.githubusercontent.com/u/66525499?v=4", + "profile": "https://github.com/FreemRL", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/package.json b/package.json index 381f59a18..aca6d858b 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "redux-persist": "^6.0.0", "remarkable": "^2.0.1", "reselect": "^4.1.5", + "styled-components": "^6.0.8", "swiper": "^9.3.2", "url": "^0.11.0", "web-vitals": "^2.1.0", @@ -125,6 +126,7 @@ ] }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/dompurify": "^3.0.2", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.41.0", diff --git a/src/common/activities/ActivityBanner.jsx b/src/common/activities/ActivityBanner.jsx index 2581eb858..2c928b70d 100644 --- a/src/common/activities/ActivityBanner.jsx +++ b/src/common/activities/ActivityBanner.jsx @@ -9,6 +9,7 @@ import { UMAMI_EVENTS } from 'constants'; function ActivityBanner({ currentActivity }) { const { data } = useFetch(`${process.env.REACT_APP_PLAY_API_URL}/react-play`); + const formatter = Intl.NumberFormat('en', { notation: 'compact' }); const activity = activities.filter((a) => a.id === currentActivity); const { name, subtitle, description, logo, heroImage } = activity[0]; @@ -50,7 +51,8 @@ function ActivityBanner({ currentActivity }) { GitHub{' '}
-
{data.stargazers_count}
+ {' '} +
{formatter.format(data.stargazers_count)}
{' '}
diff --git a/src/common/defaultBanner/DefaultBanner.jsx b/src/common/defaultBanner/DefaultBanner.jsx index 869a2781b..d2e437285 100644 --- a/src/common/defaultBanner/DefaultBanner.jsx +++ b/src/common/defaultBanner/DefaultBanner.jsx @@ -8,6 +8,7 @@ import { UMAMI_EVENTS } from 'constants'; const DefaultBanner = () => { const { data } = useFetch(`${process.env.REACT_APP_PLAY_API_URL}/react-play`); + const formatter = Intl.NumberFormat('en', { notation: 'compact' }); return (
@@ -36,7 +37,7 @@ const DefaultBanner = () => { GitHub{' '}
-
{data.stargazers_count}
+
{formatter.format(data.stargazers_count)}
{' '}
diff --git a/src/plays/hangman-game/HangmanGame.tsx b/src/plays/hangman-game/HangmanGame.tsx new file mode 100644 index 000000000..675b59e6d --- /dev/null +++ b/src/plays/hangman-game/HangmanGame.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PlayHeader from 'common/playlists/PlayHeader'; +import Main from './components/Main'; +import './styles.css'; + +function HangmanGame(props: any) { + // Your Code Start below. + return ( + <> +
+ +
+ {/* Your Code Starts Here */} +
+ {/* Your Code Ends Here */} +
+
+ + ); +} + +export default HangmanGame; diff --git a/src/plays/hangman-game/Readme.md b/src/plays/hangman-game/Readme.md new file mode 100644 index 000000000..13102f209 --- /dev/null +++ b/src/plays/hangman-game/Readme.md @@ -0,0 +1,44 @@ +# Hangman Game + +Hangman is an old school favorite, a word game where the goal is simply to find the missing word or words. You will be presented with a number of blank spaces representing the missing letters you need to find. Use the keyboard to guess a letter. + +## Play Demographic + +- Language: ts +- Level: Intermediate + +## Creator Information + +- User: ANKITy102 +- Gihub Link: https://github.com/ANKITy102 +- Blog: +- Video: + +## Implementation Details + +The Hangman game is structured around four major components, each designed for enhanced aesthetics and responsiveness using the Styled Components library: + +1. Drawing.tsx +The Drawing.tsx component plays a crucial role in rendering the visual representation of the Hangman figure as the game progresses. It provides a visual cue to the player regarding their current progress and incorrect guesses. + +2. Word.tsx +The Word.tsx component is responsible for managing the user's input and displaying the correct answer. It ensures a seamless interaction between the player's guesses and the hidden word, providing real-time feedback on the correctness of their choices. + +3. Keyboard.tsx +The Keyboard.tsx component is designed to facilitate user input. It presents an interactive keyboard to players, allowing them to select letters as guesses. This component enhances the user experience by making it intuitive and straightforward to make guesses. + +4. Main.tsx +The Main.tsx component serves as the central hub where all other components are integrated, resulting in the complete Hangman game experience. It orchestrates the flow of the game, including initializing the game state, tracking guessed letters, and determining whether the player has won or lost. + +In addition to these core components, the game relies on a wordList.json file, which contains a collection of hints and words used throughout the gameplay. These hints provide context to players and make the game more engaging and challenging. + +This modular and organized structure ensures that the Hangman game is not only enjoyable but also maintainable and extensible, making it an excellent showcase of best practices in React development. + + +## Consideration + +Update all considerations(if any) + +## Resources + +Update external resources(if any) diff --git a/src/plays/hangman-game/components/Drawing.tsx b/src/plays/hangman-game/components/Drawing.tsx new file mode 100644 index 000000000..a6d75c7df --- /dev/null +++ b/src/plays/hangman-game/components/Drawing.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import styled from 'styled-components'; +import { + Head, + Body, + RightArm, + LeftArm, + RightLeg, + LeftLeg, + Element4, + Element3, + Element2, + Element1 +} from '../styled-components'; + +const BodyParts = [Head, Body, RightArm, LeftArm, RightLeg, LeftLeg]; +const Gallows = [Element4, Element3, Element2, Element1]; + +interface DrawingProps { + numberOfGuesses: number; +} + +export default function Drawing({ numberOfGuesses }: DrawingProps) { + return ( +
+ {BodyParts.slice(0, numberOfGuesses).map((Component, id) => ( + + ))} + {Gallows.map((Component, id) => ( + + ))} +
+ ); +} diff --git a/src/plays/hangman-game/components/Keyboard.tsx b/src/plays/hangman-game/components/Keyboard.tsx new file mode 100644 index 000000000..84b1bd5cb --- /dev/null +++ b/src/plays/hangman-game/components/Keyboard.tsx @@ -0,0 +1,62 @@ +import { useEffect } from 'react'; +import React from 'react'; +import styled from 'styled-components'; +import { KEYS } from '../constants/constants'; +import { Key, KeyboardGrid, KeyContainer } from '../styled-components'; + +interface KeyboardProps { + correctLetters: string[]; + incorrectLetters: string[]; + addGuessedLetter: (letter: string) => void; + disabled?: boolean; +} + +export default function Keyboard({ + correctLetters, + incorrectLetters, + addGuessedLetter, + disabled = false +}: KeyboardProps) { + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + const key = event.key.toLowerCase(); + if ( + !disabled && + KEYS.includes(key) && + !correctLetters.includes(key) && + !incorrectLetters.includes(key) + ) { + addGuessedLetter(key); + } + }; + + window.addEventListener('keydown', handleKeyPress); + + return () => { + window.removeEventListener('keydown', handleKeyPress); + }; + }, [correctLetters, incorrectLetters, addGuessedLetter, disabled]); + + return ( + + + {KEYS.map((key) => { + const active = correctLetters.includes(key); + const inActive = incorrectLetters.includes(key); + + return ( + addGuessedLetter(key)} + > + {key} + + ); + })} + + + ); +} diff --git a/src/plays/hangman-game/components/Main.tsx b/src/plays/hangman-game/components/Main.tsx new file mode 100644 index 000000000..6bc5754b5 --- /dev/null +++ b/src/plays/hangman-game/components/Main.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import Drawing from './Drawing'; +import words from '../constants/wordList.json'; +import Keyboard from './Keyboard'; +import Word from './Word'; +import { + BigContainer, + Container, + EndGame, + P, + Span, + Title, + TryAgainButton +} from '../styled-components'; +import { VscDebugRestart } from 'react-icons/vsc'; + +function getRandomWord(arr: { [key: string]: string }[]) { + const obj = arr[Math.floor(Math.random() * arr.length)]; + const word = Object.values(obj)[0] as string; + + return { question: Object.keys(obj)[0], word }; +} + +export default function App() { + const [wordToGuess, setWordToGuess] = useState(''); + const [hintToGuess, setHintToGuess] = useState(''); + const [guessedLetters, setGuessedLetters] = useState([]); + + const incorrectLetters = guessedLetters.filter((letter) => !wordToGuess.includes(letter)); + + const isLoser = incorrectLetters.length >= 6; + const isWinner = wordToGuess.split('').every((letter) => guessedLetters.includes(letter)); + const isGameCompleted = isWinner || isLoser; + + const addGuessedLetter = (letter: string) => { + if (!guessedLetters.includes(letter)) { + setGuessedLetters((currentLetters) => [...currentLetters, letter]); + } + }; + + const restartGame = () => { + const { word, question } = getRandomWord(words); + setHintToGuess(question); + setWordToGuess(word); + setGuessedLetters([]); + }; + + useEffect(() => { + restartGame(); + // eslint-disable-next-line + }, []); + + return ( + + + Hangman + + {!isGameCompleted ? ( + + ) : ( + + {isWinner && 'You are a winner!'} + {isLoser && 'Nice try...'} + + )} + {!isGameCompleted && ( +

+ Question: {hintToGuess} +

+ )} + + + {isGameCompleted && ( + + + + )} + + wordToGuess.includes(letter))} + disabled={isWinner || isLoser} + incorrectLetters={incorrectLetters} + /> +
+
+ ); +} diff --git a/src/plays/hangman-game/components/Word.tsx b/src/plays/hangman-game/components/Word.tsx new file mode 100644 index 000000000..b25d22fdf --- /dev/null +++ b/src/plays/hangman-game/components/Word.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Border, LetterComponent, WordContainer } from '../styled-components'; + +interface WordProps { + guessedLetters: string[]; + wordToGuess: string; + reveal?: boolean; +} + +export default function Word({ guessedLetters, wordToGuess, reveal = false }: WordProps) { + return ( + + {wordToGuess.split('').map((letter, id) => ( + + + {letter} + + + ))} + + ); +} diff --git a/src/plays/hangman-game/constants/constants.tsx b/src/plays/hangman-game/constants/constants.tsx new file mode 100644 index 000000000..f2fb8c576 --- /dev/null +++ b/src/plays/hangman-game/constants/constants.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +export const KEYS = [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z' +]; diff --git a/src/plays/hangman-game/constants/wordList.json b/src/plays/hangman-game/constants/wordList.json new file mode 100644 index 000000000..2a4b81ac9 --- /dev/null +++ b/src/plays/hangman-game/constants/wordList.json @@ -0,0 +1,51 @@ +[ + {"A large, flightless bird":"ostrich"}, + {"A nocturnal flying mammal":"bat"}, + {"A cold-blooded reptile often kept as a pet":"snake"}, + {"A four-legged, domesticated animal that barks":"dog"}, + {"A small, colorful insect that flies and collects nectar":"butterfly"}, + {"A large, long-necked animal native to Africa":"giraffe"}, + {"A popular citrus fruit":"orange"}, + {"A large, ferocious feline":"tiger"}, + {"A frozen dessert often served in a cone":"ice cream"}, + {"A yellow, crescent-shaped fruit":"banana"}, + {"A small, burrowing rodent":"mole"}, + {"A mythical creature with the body of a lion and the head of an eagle":"griffin"}, + {"A cold and creamy dairy product often used as a topping":"whipped cream"}, + {"A small, green vegetable often used in salads":"cucumber"}, + {"A fast-running bird with long legs":"ostrich"}, + {"A round, red fruit often associated with Valentine's Day":"strawberry"}, + {"A domesticated animal known for producing milk":"cow"}, + {"A venomous arachnid with eight legs":"spider"}, + {"A large, slow-moving mammal known for its long trunk":"elephant"}, + {"A popular seafood delicacy often served with butter":"lobster"}, + {"A fast, graceful mammal known for its long neck":"gazelle"}, + {"A hot, caffeinated beverage often served in the morning":"coffee"}, + {"A tropical fruit with a tough, spiky outer shell":"pineapple"}, + {"A sweet, sticky substance made by bees":"honey"}, + {"A large, striped big cat known for its roar":"lion"}, + {"A green vegetable used in salads and sandwiches":"lettuce"}, + {"A small, furry animal that hops":"rabbit"}, + {"A delicious, circular baked good often topped with icing":"doughnut"}, + {"A sour, yellow fruit often used in pies":"lemon"}, + {"A fast-swimming marine mammal with sharp teeth":"dolphin"}, + {"A small, red fruit often used in jams and jellies":"strawberry"}, + {"A cold, frozen dessert often served in a cup or cone":"ice cream"}, + {"A large, powerful bird of prey":"eagle"}, + {"A popular Italian pasta dish with tomato sauce":"spaghetti"}, + {"A long, thin pasta often used in Asian cuisine":"noodle"}, + {"A tiny, red fruit often used in salsa":"tomato"}, + {"A large, leafy green vegetable often used in salads":"spinach"}, + {"A small, furry rodent often kept as a pet":"hamster"}, + {"A sweet, sticky substance used to sweeten food and drinks":"sugar"}, + {"A popular breakfast food made from ground grains":"cereal"}, + {"A small, green vegetable often used in stir-fry":"pea"}, + {"A tropical fruit with a tough outer shell and sweet flesh":"coconut"}, + {"A round, orange fruit often associated with Halloween":"pumpkin"}, + {"A cold, fizzy beverage often served in cans or bottles":"soda"}, + {"A warm, comforting beverage often made from tea leaves":"chai"}, + {"A small, round fruit often used in pies and cobblers":"cherry"}, + {"A sweet, brown substance often used as a topping for pancakes":"syrup"}, + {"A delicious, creamy dairy product often used in desserts":"cream"} + ] + \ No newline at end of file diff --git a/src/plays/hangman-game/cover.png b/src/plays/hangman-game/cover.png new file mode 100644 index 000000000..b5b2e4584 Binary files /dev/null and b/src/plays/hangman-game/cover.png differ diff --git a/src/plays/hangman-game/styled-components.tsx b/src/plays/hangman-game/styled-components.tsx new file mode 100644 index 000000000..dd0f1e6d6 --- /dev/null +++ b/src/plays/hangman-game/styled-components.tsx @@ -0,0 +1,311 @@ +import styled from 'styled-components'; + +/** ************************************* + * Main's Styled Components + ***************************************/ +export const BigContainer = styled.div` + height: 100%; + width: 100%; +`; + +export const Container = styled.div` + margin: 0 0.5rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 2rem; + min-height: 100vh; + max-width: 800px; + font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, + Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + position: relative; + padding-bottom: 10px; + @media (min-width: 768px) { + margin: 0 auto; + } +`; + +export const P = styled.p` + margin-bottom: -20px; + font-size: 20px; +`; + +export const Span = styled.span` + font-weight: bold; +`; + +export const Title = styled.h1` + padding: 15px; +`; + +interface Key { + isWinner: boolean; +} + +export const EndGame = styled.button` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + text-align: center; + font-size: 1.3rem; + font-weight: bold; + color: ${({ isWinner }) => (isWinner ? 'green' : 'red')}; + + @media (min-width: 768px) { + font-size: 2rem; + } +`; + +export const TryAgainButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + padding: 0.3rem; + border: 3px solid black; + border-radius: 1rem; + font-size: 2.5rem; + background: none; + color: black; + cursor: pointer; + + &:hover, + &:focus { + background: #16a085; + } + + @media (min-width: 768px) { + font-size: 2.3rem; + } +`; + +/** ************************************* + * Keyboard's Styled Components + ***************************************/ +export const KeyContainer = styled.div` + align-self: stretch; +`; + +export const KeyboardGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(50px, 1fr)); + gap: 0.5rem; + width: 100%; +`; + +export interface KeyProps { + active: boolean; + inActive: boolean; +} + +export const Key = styled.button` + aspect-ratio: 1/1; + width: 100%; + border: 3px solid black; + border-radius: 1rem; + font-size: 2rem; + font-family: monospace; + text-transform: uppercase; + font-weight: bold; + background: ${({ active }) => (active ? '#16A085' : 'none')}; + color: ${({ active }) => (active ? 'white' : 'black')}; + opacity: ${({ inActive }) => (inActive ? '0.3' : '1')}; + cursor: pointer; + + &:hover:not(:disabled), + &:focus:not(:disabled) { + background-color: #f4d03f; + } + + &:disabled { + cursor: not-allowed; + } + + @media (min-width: 768px) { + font-size: 2.3rem; + } +`; + +/** ************************************* + * Drawing's Styled Components + ***************************************/ +export const Head = styled.div` + width: 50px; + height: 50px; + border-radius: 100%; + border: 10px solid black; + position: absolute; + top: 40px; + right: -20px; + + @media (min-width: 768px) { + top: 50px; + } +`; + +export const Body = styled.div` + width: 10px; + height: 80px; + background: black; + position: absolute; + top: 90px; + right: 0; + + @media (min-width: 768px) { + height: 100px; + top: 90px; + } +`; + +export const RightArm = styled.div` + width: 70px; + height: 10px; + background: black; + position: absolute; + top: 110px; + right: -70px; + transform: rotate(-30deg); + transform-origin: left bottom; + + @media (min-width: 768px) { + width: 100px; + top: 130px; + right: -100px; + } +`; + +export const LeftArm = styled.div` + width: 70px; + height: 10px; + background: black; + position: absolute; + top: 110px; + right: 10px; + transform: rotate(30deg); + transform-origin: right bottom; + + @media (min-width: 768px) { + width: 100px; + top: 130px; + } +`; + +export const RightLeg = styled.div` + width: 80px; + height: 10px; + background: black; + position: absolute; + top: 160px; + right: -70px; + transform: rotate(60deg); + transform-origin: left bottom; + + @media (min-width: 768px) { + width: 100px; + top: 180px; + right: -90px; + } +`; + +export const LeftLeg = styled.div` + width: 80px; + height: 10px; + background: black; + position: absolute; + top: 160px; + right: 0; + transform: rotate(-60deg); + transform-origin: right bottom; + + @media (min-width: 768px) { + width: 100px; + top: 180px; + } +`; + +export const Element1 = styled.div` + height: 10px; + width: 120px; + background: black; + + @media (min-width: 768px) { + height: 10px; + width: 200px; + } +`; + +export const Element2 = styled.div` + margin-left: 60px; + height: 300px; + width: 10px; + background: black; + + @media (min-width: 768px) { + margin-left: 100px; + height: 320px; + width: 10px; + } +`; + +export const Element3 = styled.div` + margin-left: 60px; + height: 10px; + width: 150px; + background: black; + + @media (min-width: 768px) { + margin-left: 100px; + height: 10px; + width: 200px; + } +`; + +export const Element4 = styled.div` + position: absolute; + top: 0; + right: 0; + height: 40px; + width: 10px; + background: black; + + @media (min-width: 768px) { + height: 50px; + } +`; + +/** ************************************* + * Word's Styled Components + ***************************************/ +export const WordContainer = styled.div` + padding: 0.5rem; + display: flex; + gap: 1rem; + max-width: 100vw; + font-size: 3rem; + font-weight: bold; + text-transform: uppercase; + font-family: monospace; + overflow-x: auto !important; + + @media (min-width: 768px) { + font-size: 5rem; + } +`; + +export const Border = styled.span` + border-bottom: 0.5rem solid black; +`; + +interface LetterProps { + guessedLetters: string[]; + letter: string; + reveal?: boolean; +} + +export const LetterComponent = styled.span` + visibility: ${({ guessedLetters, letter, reveal }) => + guessedLetters.includes(letter) || reveal ? 'visible' : 'hidden'}; + color: ${({ guessedLetters, letter, reveal }) => + !guessedLetters.includes(letter) && reveal ? '#d30000' : 'black'}; +`; diff --git a/src/plays/hangman-game/styles.css b/src/plays/hangman-game/styles.css new file mode 100644 index 000000000..f31432943 --- /dev/null +++ b/src/plays/hangman-game/styles.css @@ -0,0 +1,2 @@ +/* enter stlyes here */ +