aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/actions/build/action.yml18
-rw-r--r--.github/workflows/build_website.yml13
-rw-r--r--.github/workflows/deploy_website.yml40
-rw-r--r--.husky/commit-msg4
-rw-r--r--server/index.ts51
-rw-r--r--src/app.tsx15
-rw-r--r--src/components/Footer/index.tsx9
-rw-r--r--src/constants/index.ts2
-rw-r--r--src/data/songs.ts4
-rw-r--r--src/helpers/fetchSolution.ts52
-rw-r--r--src/helpers/fetchSongs.ts51
11 files changed, 172 insertions, 87 deletions
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
deleted file mode 100644
index e4e161f..0000000
--- a/.github/actions/build/action.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: Build Website
-runs:
- using: "composite"
- steps:
- - name: Enable Corepack
- shell: bash
- run: corepack enable
- - name: Set Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20.x
- cache: 'yarn'
- - name: Install dependencies
- shell: bash
- run: yarn install --immutable
- - name: Run build
- shell: bash
- run: yarn build
diff --git a/.github/workflows/build_website.yml b/.github/workflows/build_website.yml
deleted file mode 100644
index 56a2200..0000000
--- a/.github/workflows/build_website.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-name: Build Website
-
-on:
- pull_request:
- branches: [ "master" ]
- workflow_dispatch:
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: ./.github/actions/build
diff --git a/.github/workflows/deploy_website.yml b/.github/workflows/deploy_website.yml
deleted file mode 100644
index 7c0557e..0000000
--- a/.github/workflows/deploy_website.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-# Simple workflow for deploying static content to GitHub Pages
-name: Deploy Website
-
-on:
- # Runs on pushes targeting the default branch
- push:
- branches: ["master"]
-
- # Allows you to run this workflow manually from the Actions tab
- workflow_dispatch:
-
-# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
-permissions:
- contents: read
- pages: write
- id-token: write
-
-# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
-# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
-concurrency:
- group: "pages"
- cancel-in-progress: false
-
-jobs:
- # Single deploy job since we're just deploying
- deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: ./.github/actions/build
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: './build'
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.husky/commit-msg b/.husky/commit-msg
deleted file mode 100644
index 617efbd..0000000
--- a/.husky/commit-msg
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-npx --no-install commitlint --edit
diff --git a/server/index.ts b/server/index.ts
new file mode 100644
index 0000000..bf4cafd
--- /dev/null
+++ b/server/index.ts
@@ -0,0 +1,51 @@
+import express from 'express';
+import path from 'path';
+import crypto from 'crypto';
+import { songs } from '../src/data/songs';
+import { startDate } from '../src/constants/startDate';
+import cors from 'cors';
+
+
+const app = express();
+app.use(cors());
+app.use(express.json());
+
+const PORT = process.env.PORT || 3001;
+const SALT = process.env.HEARDLE_SALT ?? 'changeme';
+
+function getDailyKey(): Buffer {
+ const date = new Date().toISOString().split('T')[0];
+ return crypto.pbkdf2Sync(date, SALT, 100_000, 32, 'sha256');
+}
+
+app.get('/today', (_req, res) => {
+ const msInDay = 86_400_000;
+ const index = Math.floor((Date.now() - startDate.getTime()) / msInDay);
+ const song = songs[index % songs.length];
+
+ const key = getDailyKey();
+ const iv = crypto.randomBytes(12);
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
+ const encrypted = Buffer.concat([
+ cipher.update(JSON.stringify(song), 'utf8'),
+ cipher.final(),
+ ]);
+ res.json({
+ iv: iv.toString('hex'),
+ tag: cipher.getAuthTag().toString('hex'),
+ data: encrypted.toString('hex'),
+ });
+});
+
+app.get('/songs', (_req, res) => {
+ res.json(songs.map(({ artist, name }) => ({ artist, name })));
+});
+
+if (process.env.NODE_ENV === 'production') {
+ app.use(express.static(path.join(__dirname, '../build')));
+ app.get('*', (_req, res) => {
+ res.sendFile(path.join(__dirname, '../build', 'index.html'));
+ });
+}
+
+app.listen(PORT, () => console.log(`Server running on :${PORT}`));
diff --git a/src/app.tsx b/src/app.tsx
index 7c4c81d..4eeccc8 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -3,8 +3,8 @@ import _ from "lodash";
import { Song } from "./types/song";
import { GuessState, GuessType } from "./types/guess";
+import { getDailySolution } from "./helpers/fetchSolution";
-import { todaysSolution } from "./helpers";
import { Header, InfoPopUp, Game, Footer } from "./components";
@@ -22,6 +22,7 @@ function App() {
const [currentTry, setCurrentTry] = React.useState<number>(0);
const [selectedSong, setSelectedSong] = React.useState<Song>();
const [didGuess, setDidGuess] = React.useState<boolean>(false);
+ const [todaysSolution, setTodaysSolution] = React.useState<Song | null>(null);
const firstRun = localStorage.getItem("firstRun") === null;
@@ -59,6 +60,10 @@ function App() {
let statsVersion = JSON.parse(localStorage.getItem("version") || "1");
React.useEffect(() => {
+ getDailySolution().then(solution => setTodaysSolution(solution));
+ }, []);
+
+ React.useEffect(() => {
if (Array.isArray(stats)) {
const visitedToday = _.isEqual(
todaysSolution,
@@ -78,8 +83,6 @@ function App() {
setDidGuess(didGuess);
}
} else {
- // initialize stats
- // useEffect below does rest
stats = [];
stats.push({
solution: todaysSolution,
@@ -168,7 +171,7 @@ function App() {
let state = GuessState.Incorrect;
if (selectedSong === todaysSolution) {
state = GuessState.Correct;
- } else if (selectedSong?.artist === todaysSolution.artist) {
+ } else if (selectedSong?.artist === todaysSolution?.artist) {
state = GuessState.PartiallyCorrect
}
@@ -195,6 +198,10 @@ function App() {
}
}, [guesses, selectedSong]);
+ if (todaysSolution === null) {
+ return null;
+ }
+
return (
<main>
<Header openInfoPopUp={openInfoPopUp} />
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index e2747c3..3ba4c7c 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -20,13 +20,8 @@ export function Footer() {
return (
<footer>
<Styled.Text>
- Made with <IoHeart /> by{" "}
- <Styled.Link href="https://epicwolverine.com">
- EpicWolverine
- </Styled.Link>
- {" "}based on the work of{" "}
- <Styled.Link href="https://twitter.com/synowski_maciej">
- Maciej Synowski
+ <Styled.Link href="">
+ Source Code
</Styled.Link>
</Styled.Text>
{showDebugButton &&
diff --git a/src/constants/index.ts b/src/constants/index.ts
index bf50a38..9d6bd5f 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -1,5 +1,5 @@
export { appName } from "./appName";
export { playTimes } from "./playTimes";
-export { songs } from "./songs";
+export { songs } from "../data/songs";
export { startDate } from "./startDate";
export { theme } from "./theme";
diff --git a/src/data/songs.ts b/src/data/songs.ts
new file mode 100644
index 0000000..7b760b2
--- /dev/null
+++ b/src/data/songs.ts
@@ -0,0 +1,4 @@
+export const songs = [
+ { artist: "KiiiKiii", name: "Strawberry Cheesegame", youtubeId: "CQSY6FeT68I" },
+ { artist: "ifeye", name: "r u ok?", youtubeId: "tEF6aKhfPaQ" },
+];
diff --git a/src/helpers/fetchSolution.ts b/src/helpers/fetchSolution.ts
new file mode 100644
index 0000000..b43722a
--- /dev/null
+++ b/src/helpers/fetchSolution.ts
@@ -0,0 +1,52 @@
+import { Song } from "../types/song";
+
+const SALT = process.env.HEARDLE_SALT ?? 'changeme';
+const API_URL = process.env.HEARDLE_API_URL ?? 'http://localhost:3001';
+
+function hexToBytes(hex: string): Uint8Array {
+ const bytes = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < hex.length; i += 2) {
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
+ }
+ return bytes;
+}
+
+
+async function getDailyKey(): Promise<CryptoKey> {
+ const enc = new TextEncoder();
+ const date = new Date().toISOString().split('T')[0];
+ const keyMaterial = await crypto.subtle.importKey(
+ 'raw',
+ enc.encode(date),
+ 'PBKDF2',
+ false,
+ ['deriveKey'],
+ );
+ return crypto.subtle.deriveKey(
+ { name: 'PBKDF2', salt: enc.encode(SALT), iterations: 100_000, hash: 'SHA-256' },
+ keyMaterial,
+ { name: 'AES-GCM', length: 256 },
+ false,
+ ['decrypt'],
+ );
+}
+
+export async function getDailySolution(): Promise<Song> {
+ const solutionData = await fetch(`${API_URL}/today`);
+ if (!solutionData.ok) {
+ throw new Error(`Failed to fetch solution: ${solutionData.statusText}`);
+ }
+ const { iv, tag, data } = await solutionData.json();
+ const key = await getDailyKey();
+ const ciphertext = hexToBytes(data);
+ const authTag = hexToBytes(tag);
+ const combined = new Uint8Array(ciphertext.length + authTag.length);
+ combined.set(ciphertext);
+ combined.set(authTag, ciphertext.length);
+ const decrypted = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: hexToBytes(iv) },
+ key,
+ combined,
+ );
+ return JSON.parse(new TextDecoder().decode(decrypted)) as Song;
+}
diff --git a/src/helpers/fetchSongs.ts b/src/helpers/fetchSongs.ts
new file mode 100644
index 0000000..5ae0d83
--- /dev/null
+++ b/src/helpers/fetchSongs.ts
@@ -0,0 +1,51 @@
+import { Song } from "../types/song";
+let cachedSongs: Song[] | null = null;
+function fuzzyMatch(input: string): string {
+ return input.toLowerCase().replace(/[^0-9a-z ]/gi, '');
+}
+
+
+export async function fetchSongs(useCache=true): Promise<Song[]> {
+ if (useCache && cachedSongs) {
+ return cachedSongs;
+ }
+
+ try {
+ const response = await fetch('/songs');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const songsData: Song[] = await response.json();
+ cachedSongs = songsData;
+ return songsData;
+ } catch (error) {
+ console.error("Failed to fetch songs:", error);
+ throw error;
+ }
+}
+
+export async function searchSongs(searchTerm: string): Promise<Song[]> {
+ const songsToSearch = await fetchSongs();
+
+ const processedSearchTerm = fuzzyMatch(searchTerm);
+
+ const matchingSongs = songsToSearch
+ .filter((song: Song) => {
+ const songName = fuzzyMatch(song.name);
+ const songArtist = fuzzyMatch(song.artist);
+
+ if (songArtist.includes(processedSearchTerm) || songName.includes(processedSearchTerm)) {
+ return true;
+ }
+ return false;
+ })
+ .sort((a, b) =>
+ a.artist.toLowerCase().localeCompare(b.artist.toLocaleLowerCase())
+ || a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase())
+ );
+
+ return matchingSongs;
+}
+export function getCachedSongs(): Song[] | null {
+ return cachedSongs;
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage