From 7adae5ca9478d348c08923bbb41c144b0682450f Mon Sep 17 00:00:00 2001 From: Samir <68955143+SamirMishra27@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:02:26 +0530 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[Feature]:=20Copy=20to=20Clipboard?= =?UTF-8?q?=20Feature=20in=20Wordle=20Play=20(#1370)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: wordle tiles rendering on every user interaction This commit fixes an issue with Wordle Game Play where if you interact with the game in any form, all the wordle tiles and the app would forcefully re-render. Issue was identified with assigning a unique key to Wordle tiles. Now it assigns stable keys so it does not break and react is able to recognise it. (unique key identifier) References issue #1353. * feat: add feature to share your current game in wordle play (copy to clipboard) This commit adds a useful feature to the Wordle play game i.e, share your tiles with your friends! The button, located at the top-right of the play's screen will allow you to share an image of the current puzzle by taking a snapshot of the puzzle dom. (Using html2canvas library) Other code changes: - Add a share.svg for the button - Add styles for the copy to clipboard modal - Add a utility function to sleep - Change errorSlideRef name to notifSlideRef to be more consistent with its usage. - Remove animations of wordle tiles after some time using useEffect. - Use html2canvas for image generation. This will successfully close the issue #1358. --------- Co-authored-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com> --- src/plays/wordle/Wordle.tsx | 57 ++++++++++--- src/plays/wordle/assets/share.svg | 1 + .../wordle/components/ClipboardModal.tsx | 84 +++++++++++++++++++ src/plays/wordle/components/WordleRow.tsx | 4 + src/plays/wordle/components/WordleTile.tsx | 18 +++- src/plays/wordle/styles.css | 44 +++++++++- src/plays/wordle/utils.ts | 3 + 7 files changed, 197 insertions(+), 14 deletions(-) create mode 100644 src/plays/wordle/assets/share.svg create mode 100644 src/plays/wordle/components/ClipboardModal.tsx diff --git a/src/plays/wordle/Wordle.tsx b/src/plays/wordle/Wordle.tsx index 333480541..cb2dcc447 100644 --- a/src/plays/wordle/Wordle.tsx +++ b/src/plays/wordle/Wordle.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef, MutableRefObject, MouseEvent } from 'react import PlayHeader from 'common/playlists/PlayHeader'; // Components +import ClipboardModal from './components/ClipboardModal'; import EndScreen from './components/EndScreen'; import KeyboardKey from './components/KeyboardKey'; import WordleRow from './components/WordleRow'; @@ -15,6 +16,7 @@ import { AllTimeStats, TileRow, WordleAction } from './types'; import './styles.css'; import WORDLE_WORDS from './data/words'; import backspace from './assets/backspace.svg'; +import share from './assets/share.svg'; // Get a random wordle word from data function getRandomWordleWord() { @@ -47,9 +49,14 @@ function Wordle(props: any): JSX.Element { correct: Array() }); - const errorSlideRef = useRef() as MutableRefObject; + const notifSlideRef = useRef() as MutableRefObject; + const wordleRef = useRef() as MutableRefObject; + const [allTimeStats, setStats] = useState(null); + const [wordleCopy, setWordleCopy] = useState(null); + const [gameOver, setGameOver] = useState(false); + const removeCopy = () => setWordleCopy(null); function reset() { // Reset and start a new game! @@ -63,27 +70,28 @@ function Wordle(props: any): JSX.Element { setRow(0); setIndex(0); + // Resetting letter status to remove keyboard formatting setLetterStatus({ wrong: Array(), misplaced: Array(), correct: Array() }); // This method removes all children from element node - errorSlideRef.current.replaceChildren(); + notifSlideRef.current.replaceChildren(); setStats(null); setGameOver(false); } - function pushError(string: string) { + function pushNotif(string: string) { const errorMsg = createElement( 'div', 'bg-slate-200 p-2 text-center font-semibold rounded-lg delete-after', string ); - errorSlideRef.current.insertBefore(errorMsg, errorSlideRef.current.firstChild); + notifSlideRef.current.insertBefore(errorMsg, notifSlideRef.current.firstChild); setTimeout(() => { - errorSlideRef.current.removeChild(errorMsg); + notifSlideRef.current.removeChild(errorMsg); }, ERR_EXP_AFTER); } @@ -119,7 +127,7 @@ function Wordle(props: any): JSX.Element { function evaluateRow() { if (currIndex !== 5) { - return pushError('Not enough words'); + return pushNotif('Not enough words'); } const tileRow = tiles[currRow]; @@ -127,7 +135,7 @@ function Wordle(props: any): JSX.Element { // If the word is not in global list if (!WORDLE_WORDS.includes(currGuess.toLowerCase())) { - return pushError('Not in word list'); + return pushNotif('Not in word list'); } // If the guess is correct @@ -141,7 +149,7 @@ function Wordle(props: any): JSX.Element { 'bg-slate-200 p-2 text-center font-semibold rounded-lg text-lg', 'Well Done!' ); - errorSlideRef.current.insertBefore(answerElem, errorSlideRef.current.firstChild); + notifSlideRef.current.insertBefore(answerElem, notifSlideRef.current.firstChild); }, 2.5 * 1000); updateLetterStatus(tileRow); @@ -162,7 +170,7 @@ function Wordle(props: any): JSX.Element { 'bg-slate-200 p-2 text-center font-semibold rounded-lg text-lg font-wordle text-black', wordleWord.toUpperCase() ); - errorSlideRef.current.insertBefore(answerElem, errorSlideRef.current.firstChild); + notifSlideRef.current.insertBefore(answerElem, notifSlideRef.current.firstChild); setTimeout(() => { const updatedStats = setLocalData('LOSS', currRow); @@ -236,12 +244,39 @@ function Wordle(props: any): JSX.Element {
+ + {allTimeStats && } + {wordleCopy && ( + + )} -
+
diff --git a/src/plays/wordle/assets/share.svg b/src/plays/wordle/assets/share.svg new file mode 100644 index 000000000..3fd25b3c4 --- /dev/null +++ b/src/plays/wordle/assets/share.svg @@ -0,0 +1 @@ +share \ No newline at end of file diff --git a/src/plays/wordle/components/ClipboardModal.tsx b/src/plays/wordle/components/ClipboardModal.tsx new file mode 100644 index 000000000..25cf5b578 --- /dev/null +++ b/src/plays/wordle/components/ClipboardModal.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { MutableRefObject, useEffect, useRef } from 'react'; +import html2canvas from 'html2canvas'; +import { seconds } from '../utils'; + +export default function ClipboardModal(props: { + wordleCopy: Node | null; + removeCopy: () => void; + pushNotif: (s: string) => void; +}) { + const { wordleCopy, removeCopy, pushNotif } = props; + const copyableRef = useRef() as MutableRefObject; + const selfRef = useRef() as MutableRefObject; + + async function copyTilesToClipboard() { + // Render the elements into a canvas + const canvas = await html2canvas(copyableRef.current); + + // Convert to a blob type then append to Clipboard + canvas.toBlob((blob) => { + if (!blob) return; + const clipboardItem = new ClipboardItem({ 'image/png': blob }); + navigator.clipboard.write([clipboardItem]); + }); + // Send notification + pushNotif('Copied to clipboard!'); + + // Clean up + disappear(); + } + + function disappear() { + selfRef.current.classList.remove('appear'); + setTimeout(() => removeCopy(), seconds(1)); + } + + useEffect(() => { + if (wordleCopy) copyableRef.current.appendChild(wordleCopy); + + return; + }, []); + + return ( +
+
+

+ Can you help me solve this Wordle? 🤔 +

+
+ + +
+ ); +} diff --git a/src/plays/wordle/components/WordleRow.tsx b/src/plays/wordle/components/WordleRow.tsx index c44385b7a..cc915f977 100644 --- a/src/plays/wordle/components/WordleRow.tsx +++ b/src/plays/wordle/components/WordleRow.tsx @@ -23,6 +23,7 @@ export default function WordleRow(props: { rowNo: number; tileRow: TileRow; word if (wordleWord.includes(letter) && wordleWord.slice(index, index + 1) === letter) { return ( ; + useEffect(() => { + if (guessed) + setTimeout(() => { + tileRef.current.style.backgroundColor = style; + tileRef.current.classList.remove('roll-tile'); + tileRef.current.classList.add('guessed'); + }, seconds(2)); + else tileRef.current.style.backgroundColor = ''; + }); + return (
{tile} diff --git a/src/plays/wordle/styles.css b/src/plays/wordle/styles.css index 886e153fa..b0b0e84c4 100644 --- a/src/plays/wordle/styles.css +++ b/src/plays/wordle/styles.css @@ -1,4 +1,11 @@ -/* enter stlyes here */ +@tailwind base; +@layer base { + img { + @apply inline-block + } +} + +/* enter styles here */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); .wordle-game-body { @@ -51,6 +58,15 @@ border-color: var(--correct); } +/* Main styles */ +.clipboard-modal.appear { + animation: copy-modal-appear 600ms ease-in-out 0s 1 normal forwards; +} + +.clipboard-modal:not(.appear) { + animation: copy-modal-disappear 600ms ease-in-out 0s 1 normal forwards; +} + .end-screen { animation: end-screen-appear 600ms ease-in-out 0s 1 normal forwards; } @@ -64,6 +80,10 @@ border: 2px solid var(--correct); } +.filled.guessed { + border: 2px solid var(--color); +} + .wordle-tile:not(.filled) { animation: drop 115ms ease-in-out 0s 1 normal forwards; border: var(--unfilled-border); @@ -158,3 +178,25 @@ opacity: 1; } } + +@keyframes copy-modal-appear { + from { + transform: translateY(-40px); + opacity: 0; + } + to { + transform: translateY(0px); + opacity: 1; + } +} + +@keyframes copy-modal-disappear { + from { + transform: translateY(0px); + opacity: 1; + } + to { + transform: translateY(-40px); + opacity: 0; + } +} \ No newline at end of file diff --git a/src/plays/wordle/utils.ts b/src/plays/wordle/utils.ts index cf0cf2723..541059460 100644 --- a/src/plays/wordle/utils.ts +++ b/src/plays/wordle/utils.ts @@ -53,3 +53,6 @@ export function setLocalData(result: 'WIN' | 'LOSS', attempt: number) { return allTimeStats; } + +// Return milliseconds as seconds +export const seconds = (s: number) => s * 1000;