Skip to content

Commit

Permalink
✨ [Feature]: Copy to Clipboard Feature in Wordle Play (#1370)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
SamirMishra27 and priyankarpal authored Oct 30, 2023
1 parent 777bb90 commit 7adae5c
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 14 deletions.
57 changes: 46 additions & 11 deletions src/plays/wordle/Wordle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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() {
Expand Down Expand Up @@ -47,9 +49,14 @@ function Wordle(props: any): JSX.Element {
correct: Array<string>()
});

const errorSlideRef = useRef() as MutableRefObject<HTMLDivElement>;
const notifSlideRef = useRef() as MutableRefObject<HTMLDivElement>;
const wordleRef = useRef() as MutableRefObject<HTMLDivElement>;

const [allTimeStats, setStats] = useState<AllTimeStats | null>(null);
const [wordleCopy, setWordleCopy] = useState<Node | null>(null);

const [gameOver, setGameOver] = useState(false);
const removeCopy = () => setWordleCopy(null);

function reset() {
// Reset and start a new game!
Expand All @@ -63,27 +70,28 @@ function Wordle(props: any): JSX.Element {

setRow(0);
setIndex(0);
// Resetting letter status to remove keyboard formatting
setLetterStatus({
wrong: Array<string>(),
misplaced: Array<string>(),
correct: Array<string>()
});
// 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);
}

Expand Down Expand Up @@ -119,15 +127,15 @@ 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];
const currGuess = tileRow.row.join('');

// 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
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -236,12 +244,39 @@ function Wordle(props: any): JSX.Element {
<div className="w-full h-[80vh] flex flex-col lg:flex-row items-center justify-center relative overflow-hidden lg:space-x-8 space-y-4 lg:space-y-0">
<div
className="error-slide w-48 bg-transparent absolute p-3 space-y-4 z-10 top-14 font-wordle"
ref={errorSlideRef}
ref={notifSlideRef}
/>

<button
className={
'copy-button w-12 sm:w-16 h-8 sm:h-10 bg-correct absolute top-0 sm:top-4 right-2 sm:right-6 rounded-2xl p-px ' +
'text-white text-sm font-medium ' +
'hover:bg-[#60a25a] transition active:bg-correct mt-4 outline-transparent'
}
data-action="Copy"
title="Copy Current Tiles"
onClick={() => setWordleCopy(wordleRef.current.cloneNode(true))}
>
<img
alt="Copy Current Tiles"
className="bg-transparent w-6 sm:w-8 h-auto m-auto"
src={share}
/>
</button>

{allTimeStats && <EndScreen allTimeStats={allTimeStats} reset={reset} />}
{wordleCopy && (
<ClipboardModal
pushNotif={pushNotif}
removeCopy={removeCopy}
wordleCopy={wordleCopy}
/>
)}

<div className="wordle w-[21rem] lg:w-[20rem] h-[24rem] flex flex-col items-center justify-evenly">
<div
className="wordle w-[21rem] lg:w-[20rem] h-[24rem] flex flex-col items-center justify-evenly"
ref={wordleRef}
>
<WordleRow rowNo={0} tileRow={tiles[0]} wordleWord={wordleWord} />
<WordleRow rowNo={1} tileRow={tiles[1]} wordleWord={wordleWord} />
<WordleRow rowNo={2} tileRow={tiles[2]} wordleWord={wordleWord} />
Expand Down
1 change: 1 addition & 0 deletions src/plays/wordle/assets/share.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions src/plays/wordle/components/ClipboardModal.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;
const selfRef = useRef() as MutableRefObject<HTMLDivElement>;

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 (
<div
className="clipboard-modal w-5/6 md:w-2/6 h-[70%] md:h-4/5 bg-default absolute z-10 rounded-lg flex flex-col items-center justify-center space-y-4 opacity-0 translate-y-[-40px] appear"
ref={selfRef}
>
<div
className="copyable-wordle h-auto flex flex-col items-center justify-evenly bg-default"
ref={copyableRef}
>
<p className="p-2 text-center font-semibold text-slate-100">
Can you help me solve this Wordle? 🤔
</p>
</div>
<button
className={
'reset-button w-32 sm:w-36 h-8 sm:h-10 bg-correct rounded-2xl text-white text-xs sm:text-sm font-medium p-px ' +
'hover:bg-[#60a25a] transition active:bg-correct mt-4'
}
onClick={() => copyTilesToClipboard()}
>
Copy to clipboard!
</button>
<button
className="w-6 md:w-10 h-auto absolute top-0 md:top-3 right-1 md:right-3 fill-white hover:fill-slate-200 active:fill-white"
onClick={() => disappear()}
>
<svg
className="w-full h-auto bg-transparent"
height="48px"
viewBox="0 0 1.44 1.44"
width="48px"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 0H1.44V1.44H0V0z" fill="none" height="24" width="24" x="0" />
<g>
<path d="M1.062 0.462l-0.085 -0.085L0.72 0.635 0.462 0.378l-0.085 0.085L0.635 0.72l-0.258 0.258 0.085 0.085L0.72 0.805l0.258 0.258 0.085 -0.085L0.805 0.72l0.258 -0.258z" />
</g>
</svg>
</button>
</div>
);
}
4 changes: 4 additions & 0 deletions src/plays/wordle/components/WordleRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<WordleTile
guessed
roll
correct={isCorrect}
index={index}
Expand All @@ -37,6 +38,7 @@ export default function WordleRow(props: { rowNo: number; tileRow: TileRow; word
) {
return (
<WordleTile
guessed
roll
correct={isCorrect}
index={index}
Expand All @@ -48,6 +50,7 @@ export default function WordleRow(props: { rowNo: number; tileRow: TileRow; word
} else {
return (
<WordleTile
guessed
roll
correct={isCorrect}
index={index}
Expand All @@ -62,6 +65,7 @@ export default function WordleRow(props: { rowNo: number; tileRow: TileRow; word
return (
<WordleTile
correct={isCorrect}
guessed={false}
index={index}
key={`tile-${rowNo}-${index}`}
roll={false}
Expand Down
18 changes: 16 additions & 2 deletions src/plays/wordle/components/WordleTile.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import React from 'react';
import { CSSProperties } from 'react';
import { CSSProperties, useRef, MutableRefObject, useEffect } from 'react';
import { seconds } from '../utils';

export default function WordleTile(props: {
correct: boolean;
guessed: boolean;
index: number;
roll: boolean;
style: string;
tile: string;
}) {
const { correct, index, roll, style, tile } = props;
const { correct, guessed, index, roll, style, tile } = props;
const inlineStyle = { '--index': `${index}s`, '--color': style } as CSSProperties;

const tileRef = useRef() as MutableRefObject<HTMLDivElement>;
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 (
<div
className={
Expand All @@ -20,6 +33,7 @@ export default function WordleTile(props: {
(roll ? 'roll-tile ' : '') +
(tile ? 'filled ' : '')
}
ref={tileRef}
style={inlineStyle}
>
{tile}
Expand Down
44 changes: 43 additions & 1 deletion src/plays/wordle/styles.css
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
}
3 changes: 3 additions & 0 deletions src/plays/wordle/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit 7adae5c

Please sign in to comment.