aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorBrendan F <EpicWolverine@users.noreply.github.com>2023-05-14 23:12:27 -0700
committerBrendan F <EpicWolverine@users.noreply.github.com>2023-05-14 23:12:27 -0700
commit737344a72d23dc97b0d0e73cc4ab7fdffd0fbf49 (patch)
tree2a915b59ab29ac79012ca3345999d9e23562d1f9 /src
parentb19a001171bd8197a30f397091d67eba5e4c1111 (diff)
Merge in react app code
From sluchajfun and youtube-heardle-template
Diffstat (limited to 'src')
-rw-r--r--src/app.styled.ts18
-rw-r--r--src/app.tsx164
-rw-r--r--src/components/Button/index.styled.ts23
-rw-r--r--src/components/Button/index.tsx19
-rw-r--r--src/components/Footer/index.styled.ts11
-rw-r--r--src/components/Footer/index.tsx17
-rw-r--r--src/components/Game/index.styled.ts8
-rw-r--r--src/components/Game/index.tsx65
-rw-r--r--src/components/Guess/index.styled.ts39
-rw-r--r--src/components/Guess/index.tsx32
-rw-r--r--src/components/Header/index.styled.ts49
-rw-r--r--src/components/Header/index.tsx26
-rw-r--r--src/components/InfoPopUp/index.styled.ts70
-rw-r--r--src/components/InfoPopUp/index.tsx36
-rw-r--r--src/components/Player/index.styled.ts48
-rw-r--r--src/components/Player/index.tsx97
-rw-r--r--src/components/Result/index.styled.ts33
-rw-r--r--src/components/Result/index.tsx74
-rw-r--r--src/components/Search/index.styled.ts88
-rw-r--r--src/components/Search/index.tsx67
-rw-r--r--src/components/YouTube/index.tsx24
-rw-r--r--src/components/index.ts10
-rw-r--r--src/constants/index.ts3
-rw-r--r--src/constants/playTimes.ts1
-rw-r--r--src/constants/songs.ts222
-rw-r--r--src/constants/theme.ts11
-rw-r--r--src/helpers/index.ts3
-rw-r--r--src/helpers/scoreToEmoji.ts32
-rw-r--r--src/helpers/searchSong.ts20
-rw-r--r--src/helpers/todaysSolution.ts12
-rw-r--r--src/index.css8
-rw-r--r--src/index.tsx20
-rw-r--r--src/react-app-env.d.ts1
-rw-r--r--src/setupTests.ts5
-rw-r--r--src/types/guess.ts7
-rw-r--r--src/types/modules/Theme.d.ts8
-rw-r--r--src/types/song.ts5
37 files changed, 1376 insertions, 0 deletions
diff --git a/src/app.styled.ts b/src/app.styled.ts
new file mode 100644
index 0000000..1c040eb
--- /dev/null
+++ b/src/app.styled.ts
@@ -0,0 +1,18 @@
+import styled from "styled-components";
+
+export const Container = styled.div`
+ width: 40%;
+
+ @media (max-width: 768px) {
+ width: 90%;
+ }
+
+ max-width: 600px;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ margin: 0 auto;
+`;
diff --git a/src/app.tsx b/src/app.tsx
new file mode 100644
index 0000000..a045bcc
--- /dev/null
+++ b/src/app.tsx
@@ -0,0 +1,164 @@
+import { event } from "react-ga";
+
+import React from "react";
+import _ from "lodash";
+
+import { Song } from "./types/song";
+import { GuessType } from "./types/guess";
+
+import { todaysSolution } from "./helpers";
+
+import { Header, InfoPopUp, Game, Footer } from "./components";
+
+import * as Styled from "./app.styled";
+
+function App() {
+ const initialGuess = {
+ song: undefined,
+ skipped: false,
+ isCorrect: undefined,
+ } as GuessType;
+
+ const [guesses, setGuesses] = React.useState<GuessType[]>(
+ Array.from({ length: 5 }).fill(initialGuess) as GuessType[]
+ );
+ const [currentTry, setCurrentTry] = React.useState<number>(0);
+ const [selectedSong, setSelectedSong] = React.useState<Song>();
+ const [didGuess, setDidGuess] = React.useState<boolean>(false);
+
+ const firstRun = localStorage.getItem("firstRun") === null;
+ let stats = JSON.parse(localStorage.getItem("stats") || "{}");
+
+ React.useEffect(() => {
+ if (Array.isArray(stats)) {
+ const visitedToday = _.isEqual(
+ todaysSolution,
+ stats[stats.length - 1].solution
+ );
+
+ if (!visitedToday) {
+ stats.push({
+ solution: todaysSolution,
+ currentTry: 0,
+ didGuess: 0,
+ });
+ } else {
+ const { currentTry, guesses, didGuess } = stats[stats.length - 1];
+ setCurrentTry(currentTry);
+ setGuesses(guesses);
+ setDidGuess(didGuess);
+ }
+ } else {
+ // initialize stats
+ // useEffect below does rest
+ stats = [];
+ stats.push({
+ solution: todaysSolution,
+ });
+ }
+ }, []);
+
+ React.useEffect(() => {
+ if (Array.isArray(stats)) {
+ stats[stats.length - 1].currentTry = currentTry;
+ stats[stats.length - 1].didGuess = didGuess;
+ stats[stats.length - 1].guesses = guesses;
+ }
+ }),
+ [guesses, currentTry, didGuess];
+
+ React.useEffect(() => {
+ localStorage.setItem("stats", JSON.stringify(stats));
+ }, [stats]);
+
+ const [isInfoPopUpOpen, setIsInfoPopUpOpen] =
+ React.useState<boolean>(firstRun);
+
+ const openInfoPopUp = React.useCallback(() => {
+ setIsInfoPopUpOpen(true);
+ }, []);
+
+ const closeInfoPopUp = React.useCallback(() => {
+ if (firstRun) {
+ localStorage.setItem("firstRun", "false");
+ setIsInfoPopUpOpen(false);
+ } else {
+ setIsInfoPopUpOpen(false);
+ }
+ }, [localStorage.getItem("firstRun")]);
+
+ const skip = React.useCallback(() => {
+ setGuesses((guesses: GuessType[]) => {
+ const newGuesses = [...guesses];
+ newGuesses[currentTry] = {
+ song: undefined,
+ skipped: true,
+ isCorrect: undefined,
+ };
+
+ return newGuesses;
+ });
+
+ setCurrentTry((currentTry) => currentTry + 1);
+
+ event({
+ category: "Game",
+ action: "Skip",
+ });
+ }, [currentTry]);
+
+ const guess = React.useCallback(() => {
+ const isCorrect = selectedSong === todaysSolution;
+
+ if (!selectedSong) {
+ alert("Choose a song");
+ return;
+ }
+
+ setGuesses((guesses: GuessType[]) => {
+ const newGuesses = [...guesses];
+ newGuesses[currentTry] = {
+ song: selectedSong,
+ skipped: false,
+ isCorrect: isCorrect,
+ };
+
+ return newGuesses;
+ });
+
+ setCurrentTry((currentTry) => currentTry + 1);
+ setSelectedSong(undefined);
+
+ if (isCorrect) {
+ setDidGuess(true);
+ }
+
+ event({
+ category: "Game",
+ action: "Guess",
+ label: `${selectedSong.artist} - ${selectedSong.name}`,
+ value: isCorrect ? 1 : 0,
+ });
+ }, [guesses, selectedSong]);
+
+ return (
+ <main>
+ <Header openInfoPopUp={openInfoPopUp} />
+ {isInfoPopUpOpen && <InfoPopUp onClose={closeInfoPopUp} />}
+ <Styled.Container>
+ <Game
+ guesses={guesses}
+ didGuess={didGuess}
+ todaysSolution={todaysSolution}
+ currentTry={currentTry}
+ setSelectedSong={setSelectedSong}
+ skip={skip}
+ guess={guess}
+ />
+ </Styled.Container>
+ {/* <Footer /> */}
+ </main>
+ );
+}
+
+export default App;
diff --git a/src/components/Button/index.styled.ts b/src/components/Button/index.styled.ts
new file mode 100644
index 0000000..1bb964f
--- /dev/null
+++ b/src/components/Button/index.styled.ts
@@ -0,0 +1,23 @@
+import styled from "styled-components";
+import { theme } from "../../constants";
+
+export const Button = styled.button<{ variant?: keyof typeof theme }>`
+ background-color: ${({ theme, variant }) =>
+ variant ? theme[variant] : theme.background100};
+
+ border-radius: 5px;
+ border: none;
+
+ color: ${({ theme }) => theme.text};
+ font-size: 1rem;
+ font-weight: 800;
+
+ width: max-content;
+ padding: 12.5px 20px;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ cursor: pointer;
+`;
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
new file mode 100644
index 0000000..7e6fa5f
--- /dev/null
+++ b/src/components/Button/index.tsx
@@ -0,0 +1,19 @@
+import React, { CSSProperties } from "react";
+
+import * as Styled from "./index.styled";
+import { theme } from "../../constants";
+
+interface Props {
+ style?: CSSProperties;
+ variant?: keyof typeof theme;
+ children: React.ReactNode;
+ onClick?: () => void;
+}
+
+export function Button({ onClick, style, variant, children }: Props) {
+ return (
+ <Styled.Button onClick={onClick} variant={variant} style={style}>
+ {children}
+ </Styled.Button>
+ );
+}
diff --git a/src/components/Footer/index.styled.ts b/src/components/Footer/index.styled.ts
new file mode 100644
index 0000000..6caa7ff
--- /dev/null
+++ b/src/components/Footer/index.styled.ts
@@ -0,0 +1,11 @@
+import styled from "styled-components";
+
+export const Text = styled.p`
+ text-align: center;
+ color: ${({ theme }) => theme.text};
+ margin-top: 50px;
+`;
+
+export const Link = styled.a`
+ color: ${({ theme }) => theme.text};
+`;
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 0000000..c5ad421
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { IoHeart } from "react-icons/io5";
+
+import * as Styled from "./index.styled";
+
+export function Footer() {
+ return (
+ <footer>
+ <Styled.Text>
+ Made with <IoHeart /> by{" "}
+ <Styled.Link href="https://twitter.com/synowski_maciej">
+ Maciej Synowski
+ </Styled.Link>
+ </Styled.Text>
+ </footer>
+ );
+}
diff --git a/src/components/Game/index.styled.ts b/src/components/Game/index.styled.ts
new file mode 100644
index 0000000..7860f98
--- /dev/null
+++ b/src/components/Game/index.styled.ts
@@ -0,0 +1,8 @@
+import styled from "styled-components";
+
+export const Buttons = styled.div`
+ margin-top: 5%;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+`;
diff --git a/src/components/Game/index.tsx b/src/components/Game/index.tsx
new file mode 100644
index 0000000..84ca282
--- /dev/null
+++ b/src/components/Game/index.tsx
@@ -0,0 +1,65 @@
+import React from "react";
+
+import { GuessType } from "../../types/guess";
+import { Song } from "../../types/song";
+import { playTimes } from "../../constants";
+
+import { Button, Guess, Player, Search, Result } from "../";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ guesses: GuessType[];
+ todaysSolution: any;
+ currentTry: number;
+ didGuess: boolean;
+ setSelectedSong: React.Dispatch<React.SetStateAction<Song | undefined>>;
+ skip: () => void;
+ guess: () => void;
+}
+
+export function Game({
+ guesses,
+ todaysSolution,
+ currentTry,
+ didGuess,
+ setSelectedSong,
+ skip,
+ guess,
+}: Props) {
+ if (didGuess || currentTry === 6) {
+ return (
+ <Result
+ didGuess={didGuess}
+ currentTry={currentTry}
+ todaysSolution={todaysSolution}
+ guesses={guesses}
+ />
+ );
+ }
+ return (
+ <>
+ {guesses.map((guess: GuessType, index) => (
+ <Guess
+ key={index}
+ guess={guess}
+ isCorrect={guess.isCorrect}
+ active={index === currentTry}
+ />
+ ))}
+ <Player id={todaysSolution.youtubeId} currentTry={currentTry} />
+ <Search currentTry={currentTry} setSelectedSong={setSelectedSong} />
+
+ <Styled.Buttons>
+ <Button onClick={skip}>
+ {currentTry === 5
+ ? "Give Up"
+ : `Skip +${playTimes[currentTry] / 1000}s`}
+ </Button>
+ <Button variant="green" onClick={guess}>
+ Submit
+ </Button>
+ </Styled.Buttons>
+ </>
+ );
+}
diff --git a/src/components/Guess/index.styled.ts b/src/components/Guess/index.styled.ts
new file mode 100644
index 0000000..5118b70
--- /dev/null
+++ b/src/components/Guess/index.styled.ts
@@ -0,0 +1,39 @@
+import styled from "styled-components";
+
+export const Container = styled.div<{
+ active: boolean;
+ isCorrect: boolean | undefined;
+}>`
+ width: 100%;
+ height: 45px;
+
+ margin: 5px auto;
+
+ display: flex;
+ align-items: center;
+
+ border-color: ${({ theme, active, isCorrect }) => {
+ if (active) {
+ return theme.border;
+ } else if (isCorrect === false) {
+ return theme.red;
+ } else {
+ return theme.border100;
+ }
+ }};
+ border-width: 1px;
+ border-radius: 5px;
+ border-style: solid;
+
+ color: ${({ theme }) => theme.text};
+`;
+
+export const Text = styled.p`
+ width: 100%;
+ height: max-content;
+
+ padding: 0px 10px;
+
+ font-size: 1rem;
+ color: ${({ theme }) => theme.text};
+`;
diff --git a/src/components/Guess/index.tsx b/src/components/Guess/index.tsx
new file mode 100644
index 0000000..2afd35c
--- /dev/null
+++ b/src/components/Guess/index.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+
+import { GuessType } from "../../types/guess";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ guess: GuessType;
+ isCorrect: boolean | undefined;
+ active: boolean;
+}
+
+export function Guess({ guess, isCorrect, active }: Props) {
+ const { song, skipped } = guess;
+ const [text, setText] = React.useState<string>("");
+
+ React.useEffect(() => {
+ if (song) {
+ setText(`${song.artist} - ${song.name}`);
+ } else if (skipped) {
+ setText("Skipped");
+ } else {
+ setText("");
+ }
+ }, [guess]);
+
+ return (
+ <Styled.Container active={active} isCorrect={isCorrect}>
+ <Styled.Text>{text}</Styled.Text>
+ </Styled.Container>
+ );
+}
diff --git a/src/components/Header/index.styled.ts b/src/components/Header/index.styled.ts
new file mode 100644
index 0000000..f06cb72
--- /dev/null
+++ b/src/components/Header/index.styled.ts
@@ -0,0 +1,49 @@
+import styled from "styled-components";
+
+export const Container = styled.header`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+
+ border-color: ${({ theme }) => theme.border};
+ border-bottom-width: 0.5px;
+ border-bottom-style: solid;
+
+ margin-bottom: 15px;
+`;
+
+export const Content = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ align-items: center;
+ justify-content: space-between;
+
+ width: 40%;
+
+ @media (max-width: 768px) {
+ width: 95%;
+ }
+
+ max-width: 650px;
+
+ svg:hover {
+ cursor: pointer;
+ opacity: 0.8;
+ }
+
+ a {
+ color: ${({ theme }) => theme.text};
+ }
+`;
+
+export const Logo = styled.h1`
+ color: ${({ theme }) => theme.text};
+ font-family: "Roboto Serif", serif;
+ text-transform: uppercase;
+ width: max-content;
+
+ -webkit-touch-callout: none;
+ user-select: none;
+`;
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
new file mode 100644
index 0000000..0c7f330
--- /dev/null
+++ b/src/components/Header/index.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { IoInformationCircleOutline } from "react-icons/io5";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ openInfoPopUp: () => void;
+}
+
+export function Header({ openInfoPopUp }: Props) {
+ return (
+ <Styled.Container>
+ <Styled.Content>
+ <IoInformationCircleOutline
+ onClick={openInfoPopUp}
+ size={30}
+ width={30}
+ height={30}
+ />
+
+ <Styled.Logo>Heardle Template</Styled.Logo>
+ <a href="#"></a>
+ </Styled.Content>
+ </Styled.Container>
+ );
+}
diff --git a/src/components/InfoPopUp/index.styled.ts b/src/components/InfoPopUp/index.styled.ts
new file mode 100644
index 0000000..6431246
--- /dev/null
+++ b/src/components/InfoPopUp/index.styled.ts
@@ -0,0 +1,70 @@
+import styled from "styled-components";
+
+export const Container = styled.div`
+ position: absolute;
+ top: 0;
+ z-index: 2;
+
+ width: 100%;
+ height: 100%;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ background-color: rgba(0, 0, 0, 0.75);
+`;
+
+export const PopUp = styled.div`
+ width: 90%;
+ max-width: 500px;
+ @media (max-width: 768px) {
+ width: 80%;
+ }
+ padding: 20px;
+
+ background-color: ${({ theme }) => theme.background100};
+
+ border-radius: 10px;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ h1 {
+ margin-bottom: 0;
+ }
+`;
+
+export const Spacer = styled.div`
+ width: 70%;
+ height: 0.2px;
+
+ margin: 20px 0;
+
+ background-color: ${({ theme }) => theme.text};
+ opacity: 0.5;
+`;
+
+export const Section = styled.div`
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ justify-content: space-between;
+
+ a {
+ color: ${({ theme }) => theme.text};
+ }
+`;
+
+export const Contact = styled.p`
+ a {
+ color: ${({ theme }) => theme.text};
+ }
+ margin-top: 5%;
+
+ font-size: 0.9rem;
+ font-weight: bold;
+ opacity: 0.5;
+`;
diff --git a/src/components/InfoPopUp/index.tsx b/src/components/InfoPopUp/index.tsx
new file mode 100644
index 0000000..eb22b19
--- /dev/null
+++ b/src/components/InfoPopUp/index.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { IoMusicalNoteOutline, IoHelpCircleOutline } from "react-icons/io5";
+import { Button } from "..";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ onClose: () => void;
+}
+
+export function InfoPopUp({ onClose }: Props) {
+ return (
+ <Styled.Container>
+ <Styled.PopUp>
+ <h1>HOW TO PLAY</h1>
+ <Styled.Spacer />
+ <Styled.Section>
+ {/* <IoMusicalNoteOutline size={50} /> */}
+ <p>
+ Listen to the intro, then find the correct Joywave song in the list.
+ </p>
+ </Styled.Section>
+ <Styled.Section>
+ {/* <IoHelpCircleOutline size={50} /> */}
+ <p>Skipped or incorrect attempts unlock more of the intro</p>
+ </Styled.Section>
+ <Styled.Section>
+ <p>Answer in as few tries as possible and share your score!</p>
+ </Styled.Section>
+ <Button variant="green" style={{ marginTop: 20 }} onClick={onClose}>
+ Play
+ </Button>
+ </Styled.PopUp>
+ </Styled.Container>
+ );
+}
diff --git a/src/components/Player/index.styled.ts b/src/components/Player/index.styled.ts
new file mode 100644
index 0000000..4200fe1
--- /dev/null
+++ b/src/components/Player/index.styled.ts
@@ -0,0 +1,48 @@
+import styled from "styled-components";
+
+export const ProgressBackground = styled.div`
+ position: relative;
+ z-index: -1;
+
+ width: 100%;
+ height: 20px;
+ background-color: ${({ theme }) => theme.gray};
+ border-radius: 2px;
+
+ margin-top: 5%;
+`;
+
+export const Progress = styled.div<{ value: number }>`
+ width: ${({ value }) => value * 6.25}%;
+ height: 20px;
+
+ align-self: flex-start;
+
+ background-color: ${({ theme }) => theme.green};
+
+ border-radius: 2px;
+
+ transition: width 0.5s;
+`;
+
+export const Separator = styled.div`
+ position: absolute;
+ top: 0;
+
+ width: 0.8px;
+ height: 100%;
+
+ background-color: ${({ theme }) => theme.border100};
+`;
+
+export const TimeStamps = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ width: 100%;
+`;
+
+export const TimeStamp = styled.p`
+ color: ${({ theme }) => theme.text};
+`;
diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx
new file mode 100644
index 0000000..82f600e
--- /dev/null
+++ b/src/components/Player/index.tsx
@@ -0,0 +1,97 @@
+import React from "react";
+import YouTube from "react-youtube";
+import { IoPlay } from "react-icons/io5";
+import { event } from "react-ga";
+
+import { playTimes } from "../../constants";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ id: string;
+ currentTry: number;
+}
+
+export function Player({ id, currentTry }: Props) {
+ const opts = {
+ width: "0",
+ height: "0",
+ };
+
+ // react-youtube doesn't export types for this
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const playerRef = React.useRef<any>(null);
+
+ const currentPlayTime = playTimes[currentTry];
+
+ const [play, setPlay] = React.useState<boolean>(false);
+
+ const [currentTime, setCurrentTime] = React.useState<number>(0);
+
+ const [isReady, setIsReady] = React.useState<boolean>(false);
+
+ React.useEffect(() => {
+ setInterval(() => {
+ playerRef.current?.internalPlayer
+ .getCurrentTime()
+ .then((time: number) => {
+ setCurrentTime(time);
+ });
+ }, 250);
+ }, []);
+
+ React.useEffect(() => {
+ if (play) {
+ if (currentTime * 1000 >= currentPlayTime) {
+ playerRef.current?.internalPlayer.pauseVideo();
+ playerRef.current?.internalPlayer.seekTo(0);
+ setPlay(false);
+ }
+ }
+ }, [play, currentTime]);
+
+ // don't call play video each time currentTime changes
+ const startPlayback = React.useCallback(() => {
+ playerRef.current?.internalPlayer.playVideo();
+ setPlay(true);
+ event({
+ category: "Player",
+ action: "Played song",
+ });
+ }, []);
+
+ const setReady = React.useCallback(() => {
+ setIsReady(true);
+ }, []);
+
+ return (
+ <>
+ <YouTube opts={opts} videoId={id} onReady={setReady} ref={playerRef} />
+ {isReady ? (
+ <>
+ <Styled.ProgressBackground>
+ {currentTime !== 0 && <Styled.Progress value={currentTime} />}
+ {playTimes.map((playTime) => (
+ <Styled.Separator
+ style={{ left: `${(playTime / 16000) * 100}%` }}
+ key={playTime}
+ />
+ ))}
+ </Styled.ProgressBackground>
+ <Styled.TimeStamps>
+ <Styled.TimeStamp>1s</Styled.TimeStamp>
+ <Styled.TimeStamp>16s</Styled.TimeStamp>
+ </Styled.TimeStamps>
+ <IoPlay
+ style={{ cursor: "pointer" }}
+ size={40}
+ color="#fff"
+ onClick={startPlayback}
+ />
+ </>
+ ) : (
+ <p>Loading player...</p>
+ )}
+ </>
+ );
+}
diff --git a/src/components/Result/index.styled.ts b/src/components/Result/index.styled.ts
new file mode 100644
index 0000000..92d51f5
--- /dev/null
+++ b/src/components/Result/index.styled.ts
@@ -0,0 +1,33 @@
+import styled from "styled-components";
+
+export const ResultTitle = styled.h1`
+ @media (max-width: 768px) {
+ text-align: center;
+ width: 100%;
+ }
+`;
+
+export const Tries = styled.h4`
+ @media (max-width: 768px) {
+ text-align: center;
+ width: 100%;
+ }
+
+ margin-top: 0;
+`;
+
+export const SongTitle = styled.h3`
+ @media (max-width: 768px) {
+ text-align: center;
+ width: 100%;
+ }
+
+ margin-top: 0;
+`;
+
+export const TimeToNext = styled.h4`
+ @media (max-width: 768px) {
+ text-align: center;
+ width: 100%;
+ }
+`;
diff --git a/src/components/Result/index.tsx b/src/components/Result/index.tsx
new file mode 100644
index 0000000..40a95fb
--- /dev/null
+++ b/src/components/Result/index.tsx
@@ -0,0 +1,74 @@
+import React from "react";
+
+import { Song } from "../../types/song";
+import { GuessType } from "../../types/guess";
+import { scoreToEmoji } from "../../helpers";
+
+import { Button } from "../Button";
+import { YouTube } from "../YouTube";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ didGuess: boolean;
+ currentTry: number;
+ todaysSolution: Song;
+ guesses: GuessType[];
+}
+
+export function Result({
+ didGuess,
+ todaysSolution,
+ guesses,
+ currentTry,
+}: Props) {
+ const hoursToNextDay = Math.floor(
+ (new Date(new Date().setHours(24, 0, 0, 0)).getTime() -
+ new Date().getTime()) /
+ 1000 /
+ 60 /
+ 60
+ );
+
+ const textForTry = ["Wow!", "Super!", "Congrats!", "Nice!"];
+
+ if (didGuess) {
+ const copyResult = React.useCallback(() => {
+ navigator.clipboard.writeText(scoreToEmoji(guesses));
+ }, [guesses]);
+
+ return (
+ <>
+ <Styled.ResultTitle>{textForTry[currentTry - 1]}</Styled.ResultTitle>
+ <Styled.SongTitle>
+ Todays song is {todaysSolution.artist} -{" "}
+ {todaysSolution.name}
+ </Styled.SongTitle>
+ <Styled.Tries>
+ You guessed it in {currentTry} {currentTry === 1 ? 'try' : 'tries'}
+ </Styled.Tries>
+ <YouTube id={todaysSolution.youtubeId} />
+ <Button onClick={copyResult} variant="green">
+ Copy results
+ </Button>
+ <Styled.TimeToNext>
+ Remember to come back in {hoursToNextDay}{" "} hours!
+ </Styled.TimeToNext>
+ </>
+ );
+ } else {
+ return (
+ <>
+ <Styled.ResultTitle>Unfortunately, thats wrong</Styled.ResultTitle>
+ <Styled.SongTitle>
+ Todays song is {todaysSolution.artist} -{" "}
+ {todaysSolution.name}
+ </Styled.SongTitle>
+ <YouTube id={todaysSolution.youtubeId} />
+ <Styled.TimeToNext>
+ Try again in {hoursToNextDay}{" "} hours
+ </Styled.TimeToNext>
+ </>
+ );
+ }
+}
diff --git a/src/components/Search/index.styled.ts b/src/components/Search/index.styled.ts
new file mode 100644
index 0000000..c1344d5
--- /dev/null
+++ b/src/components/Search/index.styled.ts
@@ -0,0 +1,88 @@
+import styled from "styled-components";
+
+export const Container = styled.div`
+ position: relative;
+
+ width: 100%;
+
+ margin-top: 5%;
+`;
+
+export const SearchContainer = styled.div`
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+ height: 50px;
+
+ border-color: ${({ theme }) => theme.border};
+ border-width: 1px;
+ border-radius: 5px;
+ border-style: solid;
+
+ color: ${({ theme }) => theme.text};
+`;
+
+export const SearchPadding = styled.div`
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+
+ padding: 0 15px;
+`;
+
+export const Input = styled.input`
+ width: 100%;
+ height: 100%;
+ margin: 0 10px;
+
+ background-color: transparent;
+ border: none;
+ outline: none !important;
+
+ color: ${({ theme }) => theme.text};
+ font-size: 1rem;
+`;
+
+export const ResultsContainer = styled.div`
+ position: absolute;
+ bottom: 50px;
+ z-index: 1;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+
+ width: 100%;
+
+ overflow-y: scroll;
+`;
+
+export const Result = styled.div`
+ padding: 1px 15px;
+
+ background-color: ${({ theme }) => theme.background100};
+
+ border-color: ${({ theme }) => theme.border};
+ border-width: 1px;
+ border-radius: 5px;
+ border-style: solid;
+
+ color: ${({ theme }) => theme.text};
+
+ cursor: pointer;
+`;
+
+export const ResultText = styled.p`
+ width: 100%;
+
+ color: ${({ theme }) => theme.text};
+ font-size: 0.9rem;
+
+ user-select: none;
+
+ &:hover {
+ opacity: 0.8;
+ }
+`;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
new file mode 100644
index 0000000..16f5c9e
--- /dev/null
+++ b/src/components/Search/index.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+import { event } from "react-ga";
+import { IoSearch } from "react-icons/io5";
+import { searchSong } from "../../helpers";
+import { Song } from "../../types/song";
+
+import * as Styled from "./index.styled";
+
+interface Props {
+ currentTry: number;
+ setSelectedSong: React.Dispatch<React.SetStateAction<Song | undefined>>;
+}
+
+export function Search({ currentTry, setSelectedSong }: Props) {
+ const [value, setValue] = React.useState<string>("");
+ const [results, setResults] = React.useState<Song[]>([]);
+
+ React.useEffect(() => {
+ if (value) {
+ setResults(searchSong(value));
+ } else if (value === "") {
+ setResults([]);
+ }
+ }, [value]);
+
+ // clear value on selection
+ React.useEffect(() => {
+ setValue("");
+ }, [currentTry]);
+
+ return (
+ <Styled.Container>
+ <Styled.ResultsContainer>
+ {results.map((song) => (
+ <Styled.Result
+ key={song.youtubeId}
+ onClick={() => {
+ setSelectedSong(song);
+ setValue(`${song.artist} - ${song.name}`);
+ setResults([]);
+
+ event({
+ category: "Player",
+ action: "Chose song",
+ label: `${song.artist} - ${song.name}`,
+ });
+ }}
+ >
+ <Styled.ResultText>
+ {song.artist} - {song.name}
+ </Styled.ResultText>
+ </Styled.Result>
+ ))}
+ </Styled.ResultsContainer>
+ <Styled.SearchContainer>
+ <Styled.SearchPadding>
+ <IoSearch size={20} />
+ <Styled.Input
+ onChange={(e) => setValue(e.currentTarget.value)}
+ placeholder="Search"
+ value={value}
+ />
+ </Styled.SearchPadding>
+ </Styled.SearchContainer>
+ </Styled.Container>
+ );
+}
diff --git a/src/components/YouTube/index.tsx b/src/components/YouTube/index.tsx
new file mode 100644
index 0000000..13ffeaa
--- /dev/null
+++ b/src/components/YouTube/index.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { default as YouTubePlayer } from "react-youtube";
+
+interface Props {
+ id: string;
+}
+
+export function YouTube({ id }: Props) {
+ return (
+ <div style={{ margin: "5% 0" }}>
+ <YouTubePlayer
+ videoId={id}
+ opts={{
+ width: "336",
+ height: "189",
+ playerVars: {
+ autoplay: 1,
+ playsinline: 1,
+ },
+ }}
+ />
+ </div>
+ );
+}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..75672f3
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,10 @@
+export { Button } from "./Button";
+export { Footer } from "./Footer";
+export { Game } from "./Game";
+export { Guess } from "./Guess";
+export { Header } from "./Header";
+export { InfoPopUp } from "./InfoPopUp";
+export { Player } from "./Player";
+export { Result } from "./Result";
+export { Search } from "./Search";
+export { YouTube } from "./YouTube";
diff --git a/src/constants/index.ts b/src/constants/index.ts
new file mode 100644
index 0000000..d396519
--- /dev/null
+++ b/src/constants/index.ts
@@ -0,0 +1,3 @@
+export { playTimes } from "./playTimes";
+export { songs } from "./songs";
+export { theme } from "./theme";
diff --git a/src/constants/playTimes.ts b/src/constants/playTimes.ts
new file mode 100644
index 0000000..3cd37ca
--- /dev/null
+++ b/src/constants/playTimes.ts
@@ -0,0 +1 @@
+export const playTimes = [1000, 2000, 4000, 7000, 11000, 16000];
diff --git a/src/constants/songs.ts b/src/constants/songs.ts
new file mode 100644
index 0000000..3d7c2a0
--- /dev/null
+++ b/src/constants/songs.ts
@@ -0,0 +1,222 @@
+export const songs = [
+ {
+ artist: "Joywave",
+ name: "Destruction",
+ youtubeId: "TA5kvZ9aqDI",
+ },
+ {
+ artist: "Wilki",
+ name: "Bohema",
+ youtubeId: "2NzeEQCJ4aM",
+ },
+ {
+ artist: "Sydney Polak",
+ name: "Otwieram wino",
+ youtubeId: "9d-gsGrnCCQ",
+ },
+ {
+ artist: "Jeden Osiem L",
+ name: "Jak zapomnieć",
+ youtubeId: "qvv7t8Y0iWk",
+ },
+ {
+ artist: "Franek Kimono",
+ name: "King Bruce Lee Karate Mistrz",
+ youtubeId: "h_QNYTrHZ2c",
+ },
+ {
+ artist: "Quebonafide",
+ name: "Madagascar",
+ youtubeId: "LWeO7aZLaZU",
+ },
+ {
+ artist: "Republika",
+ name: "Mamona",
+ youtubeId: "7qa-A_o5NTY",
+ },
+ {
+ artist: "happysad",
+ name: "Zanim pójdę",
+ youtubeId: "W1jIuwYGIrU",
+ },
+ {
+ artist: "Video",
+ name: "Wszystko Jedno",
+ youtubeId: "OUaRbA1fYSg",
+ },
+ {
+ artist: "Sanah",
+ name: "Ale jazz!",
+ youtubeId: "1jc2CdR8A-o",
+ },
+ {
+ artist: "GOLEC UORKIESTRA",
+ name: "Ściernisco",
+ youtubeId: "yrJ_lzYGJdg",
+ },
+ {
+ artist: "Dawid Podsiadło",
+ name: "Pastempomat",
+ youtubeId: "9cg4XZWMC34",
+ },
+ {
+ artist: "Taco Hemingway",
+ name: "Krwawa Jesień",
+ youtubeId: "sO7B8eNhGbY",
+ },
+ {
+ artist: "Paktofonika",
+ name: "Priorytety",
+ youtubeId: "qOMCjDM8cro",
+ },
+ {
+ artist: "Young Leosia",
+ name: "Szklanki",
+ youtubeId: "CYiGyaJyPMk",
+ },
+ {
+ artist: "Mandaryna",
+ name: "Ev'ry night",
+ youtubeId: "CmtdhCsLzK4",
+ },
+ {
+ artist: "Tymek",
+ name: "Język ciała",
+ youtubeId: "pImrABc4s58",
+ },
+ {
+ artist: "Gang z Albanii",
+ name: "Albański Raj",
+ youtubeId: "fWs8HwDtddQ",
+ },
+ {
+ artist: "Mrozu",
+ name: "Miliony monet",
+ youtubeId: "NHTTdk58z7g",
+ },
+ {
+ artist: "Popek",
+ name: "Pakistańskie disco",
+ youtubeId: "xVdn9GhwJgk",
+ },
+ {
+ artist: "Żabson & Young Igi",
+ name: "Icey",
+ youtubeId: "CcScseLdEDo",
+ },
+ {
+ artist: "Grzegorz Turnau",
+ name: "Cichosza",
+ youtubeId: "m98MBACe1xk",
+ },
+ {
+ artist: "Lady Pank",
+ name: "Kryzysowa Narzeczona",
+ youtubeId: "xPbnGCsYFAk",
+ },
+ {
+ artist: "Taconafide",
+ name: "Tamagotchi",
+ youtubeId: "odWxQ5eEnfE",
+ },
+ {
+ artist: "Dżem",
+ name: "Wehikuł czasu",
+ youtubeId: "XWcqFbMUAb4",
+ },
+ {
+ artist: "?",
+ name: "Chemia Nowej Ery",
+ youtubeId: "DeURxOmtLQA",
+ },
+ {
+ artist: "Bedoes",
+ name: "Gustaw",
+ youtubeId: "fide4cDt_xo",
+ },
+ {
+ artist: "Republika",
+ name: "Biała flaga",
+ youtubeId: "gHUDbpI8Ulk",
+ },
+ {
+ artist: "Białas",
+ name: "W imię Ojca Trapu",
+ youtubeId: "5XL78J9izsU",
+ },
+ {
+ artist: "Kult",
+ name: "Polska",
+ youtubeId: "K0NHY7wNDuM",
+ },
+ {
+ artist: "Virgin / Doda",
+ name: "Dżaga",
+ youtubeId: "_7TtBGE9MiE",
+ },
+ {
+ artist: "2115",
+ name: "Czarownica",
+ youtubeId: "QsL-XRWz4Bg",
+ },
+ {
+ artist: "Perfect",
+ name: "Niepokonani",
+ youtubeId: "HZ8qBioPlHE",
+ },
+ {
+ artist: "Lady Pank",
+ name: "Zawsze tam gdzie ty",
+ youtubeId: "YdrQxgtVBY",
+ },
+ {
+ artist: "Natalia Nykiel",
+ name: "Bądź Duży",
+ youtubeId: "qQ02hZOid0c",
+ },
+ {
+ artist: "The Dumplings",
+ name: "Przykro mi",
+ youtubeId: "kdu-E2SEGNY",
+ },
+ {
+ artist: "Wilki",
+ name: "Urke",
+ youtubeId: "anE8VjceiPw",
+ },
+ {
+ artist: "Trzeci wymiar",
+ name: "Dla mnie masz stajla",
+ youtubeId: "oKX5ldYxZKo",
+ },
+ {
+ artist: "Poparzeni Kawą Trzy",
+ name: "Byłaś dla Mnie Wszystkim",
+ youtubeId: "6i8JDcyFJV4",
+ },
+ {
+ artist: "Żabson",
+ name: "Superman",
+ youtubeId: "EDRE9Fosrlk",
+ },
+ {
+ artist: "Sentino",
+ name: "Tatuażyk",
+ youtubeId: "cXENhG0O4bQ",
+ },
+ {
+ artist: "Dawid Podsiadło",
+ name: "Trójkąty i Kwadraty",
+ youtubeId: "Wg4A_d9F7xk",
+ },
+ {
+ artist: "Poparzeni Kawą Trzy",
+ name: "Okrutna Zła i Podła",
+ youtubeId: "Tvf8jSaOJGc",
+ },
+ {
+ artist: "Sentino, Schwesta Ewa",
+ name: "Powww!",
+ youtubeId: "QhTEqLNkHcw",
+ },
+];
diff --git a/src/constants/theme.ts b/src/constants/theme.ts
new file mode 100644
index 0000000..731ef36
--- /dev/null
+++ b/src/constants/theme.ts
@@ -0,0 +1,11 @@
+export const theme = {
+ border: "#F1F7ED",
+ border100: "#5C5C5C",
+
+ text: "#FFFFFF",
+ background100: "#002E3D",
+
+ green: "#4DBB60",
+ red: "#FF0000",
+ gray: "#E6E6E6",
+} as const;
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
new file mode 100644
index 0000000..5593bc5
--- /dev/null
+++ b/src/helpers/index.ts
@@ -0,0 +1,3 @@
+export { scoreToEmoji } from "./scoreToEmoji";
+export { searchSong } from "./searchSong";
+export { todaysSolution } from "./todaysSolution";
diff --git a/src/helpers/scoreToEmoji.ts b/src/helpers/scoreToEmoji.ts
new file mode 100644
index 0000000..a118830
--- /dev/null
+++ b/src/helpers/scoreToEmoji.ts
@@ -0,0 +1,32 @@
+import { GuessType } from "../types/guess";
+
+export function scoreToEmoji(guesses: GuessType[]): string {
+ const msInDay = 86400000;
+ const startDate = new Date('4/15/2022');
+ const todaysDate = new Date();
+ const index = Math.floor((todaysDate.getTime() - startDate.getTime() )/msInDay) + 1
+ const emojis = {
+ incorrect: "🟥",
+ correct: "🟩",
+ skip: "⬜",
+ empty: "⬛️",
+ };
+ // const todaysDate = new Date();
+ const prefix = `HeardleTemplate - #${index} 🎧`;
+
+ let scoreEmoji = "";
+
+ guesses.forEach((guess: GuessType) => {
+ if (guess.isCorrect === true) {
+ scoreEmoji += emojis.correct;
+ } else if (guess.skipped === true) {
+ scoreEmoji += emojis.skip;
+ } else if (guess.isCorrect === false) {
+ scoreEmoji += emojis.incorrect;
+ } else {
+ scoreEmoji += emojis.empty;
+ }
+ });
+
+ return `${prefix} ${scoreEmoji}`;
+}
diff --git a/src/helpers/searchSong.ts b/src/helpers/searchSong.ts
new file mode 100644
index 0000000..24132f6
--- /dev/null
+++ b/src/helpers/searchSong.ts
@@ -0,0 +1,20 @@
+import { songs } from "../constants";
+import { Song } from "../types/song";
+
+export function searchSong(searchTerm: string): Song[] {
+ searchTerm = searchTerm.toLowerCase();
+
+ return songs
+ .filter((song: Song) => {
+ const songName = song.name.toLowerCase();
+ const songArtist = song.artist.toLowerCase();
+
+ if (songArtist.includes(searchTerm) || songName.includes(searchTerm)) {
+ return song;
+ }
+ })
+ .sort((a, b) =>
+ a.artist.toLowerCase().localeCompare(b.artist.toLocaleLowerCase())
+ )
+ .slice(0, 5);
+}
diff --git a/src/helpers/todaysSolution.ts b/src/helpers/todaysSolution.ts
new file mode 100644
index 0000000..50e75ce
--- /dev/null
+++ b/src/helpers/todaysSolution.ts
@@ -0,0 +1,12 @@
+import { songs } from "../constants";
+
+// const epochMs = new Date(2022, 3, 10).valueOf();
+// const now = Date.now();
+// const index = Math.floor((now - epochMs) / msInDay);
+
+const msInDay = 86400000;
+const startDate = new Date('4/15/2022');
+const todaysDate = new Date();
+const index = Math.floor((todaysDate.getTime() - startDate.getTime() )/msInDay)
+
+export const todaysSolution = songs[index % songs.length];
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..ed4230b
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,8 @@
+body {
+ @import url('https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Serif&display=swap'); margin: 0;
+ font-family: 'Raleway', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #00171F;
+ color: white;
+}
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..6d3a377
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,20 @@
+import { initialize } from "react-ga";
+
+const TRACKING_ID = "INSERT-YOUR-ID-HERE";
+initialize(TRACKING_ID);
+
+import React from "react";
+import ReactDOM from "react-dom";
+import { ThemeProvider } from "styled-components";
+import { theme } from "./constants";
+import "./index.css";
+import App from "./app";
+
+ReactDOM.render(
+ <React.StrictMode>
+ <ThemeProvider theme={theme}>
+ <App />
+ </ThemeProvider>
+ </React.StrictMode>,
+ document.getElementById("root")
+);
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..1dd407a
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import "@testing-library/jest-dom";
diff --git a/src/types/guess.ts b/src/types/guess.ts
new file mode 100644
index 0000000..a84602d
--- /dev/null
+++ b/src/types/guess.ts
@@ -0,0 +1,7 @@
+import { Song } from "./song";
+
+export type GuessType = {
+ song: Song | undefined;
+ skipped: boolean;
+ isCorrect: boolean | undefined;
+};
diff --git a/src/types/modules/Theme.d.ts b/src/types/modules/Theme.d.ts
new file mode 100644
index 0000000..ef95171
--- /dev/null
+++ b/src/types/modules/Theme.d.ts
@@ -0,0 +1,8 @@
+import "styled-components";
+import { theme } from "../../constants";
+
+type ThemeType = typeof theme;
+
+declare module "styled-components" {
+ export interface DefaultTheme extends ThemeType {}
+}
diff --git a/src/types/song.ts b/src/types/song.ts
new file mode 100644
index 0000000..b375dc0
--- /dev/null
+++ b/src/types/song.ts
@@ -0,0 +1,5 @@
+export interface Song {
+ artist: string;
+ name: string;
+ youtubeId: string;
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage