diff --git a/app/globals.css b/app/globals.css
index 42e3d8a..402347c 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -6,12 +6,9 @@
body {
color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
+ background: linear-gradient(to bottom,
transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
+ rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
margin: 0;
@@ -63,7 +60,7 @@ body {
background: #f3f4f6;
padding: 1rem;
border-radius: 0.5rem;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.player-controls {
@@ -128,7 +125,8 @@ body {
}
.guess-text {
- color: #ef4444; /* Red for wrong */
+ color: #ef4444;
+ /* Red for wrong */
}
.guess-text.skipped {
@@ -272,3 +270,84 @@ body {
.btn-primary:hover {
background: #333;
}
+
+/* Footer */
+.app-footer {
+ margin-top: auto;
+ padding: 2rem 1rem 1rem;
+ text-align: center;
+ font-size: 0.875rem;
+ color: #666;
+ border-top: 1px solid #e5e7eb;
+ width: 100%;
+}
+
+.app-footer p {
+ margin: 0;
+}
+
+.app-footer a {
+ color: #000;
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.app-footer a:hover {
+ text-decoration: underline;
+}
+
+/* Statistics */
+.statistics-container {
+ margin: 1.5rem 0;
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.5);
+ border-radius: 0.5rem;
+}
+
+.statistics-title {
+ font-size: 1.125rem;
+ font-weight: bold;
+ margin: 0 0 0.5rem 0;
+ text-align: center;
+}
+
+.statistics-total {
+ font-size: 0.875rem;
+ text-align: center;
+ margin: 0 0 1rem 0;
+ color: #666;
+}
+
+.statistics-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
+ gap: 0.75rem;
+}
+
+.stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0.75rem 0.5rem;
+ background: rgba(255, 255, 255, 0.8);
+ border-radius: 0.375rem;
+ border: 1px solid #e5e7eb;
+}
+
+.stat-badge {
+ font-size: 1.5rem;
+ margin-bottom: 0.25rem;
+}
+
+.stat-label {
+ font-size: 0.75rem;
+ color: #666;
+ margin-bottom: 0.25rem;
+ text-align: center;
+}
+
+.stat-count {
+ font-size: 1.25rem;
+ font-weight: bold;
+ color: #000;
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 42fc323..7297828 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -26,6 +26,15 @@ export default function RootLayout({
{children}
+
);
diff --git a/components/Game.tsx b/components/Game.tsx
index c13cf29..7450bae 100644
--- a/components/Game.tsx
+++ b/components/Game.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import AudioPlayer from './AudioPlayer';
import GuessInput from './GuessInput';
+import Statistics from './Statistics';
import { useGameState } from '../lib/gameState';
interface GameProps {
@@ -16,7 +17,7 @@ interface GameProps {
const UNLOCK_STEPS = [2, 4, 7, 11, 16, 30];
export default function Game({ dailyPuzzle }: GameProps) {
- const { gameState, addGuess } = useGameState();
+ const { gameState, statistics, addGuess } = useGameState();
const [hasWon, setHasWon] = useState(false);
const [hasLost, setHasLost] = useState(false);
const [shareText, setShareText] = useState('Share Result');
@@ -68,10 +69,25 @@ export default function Game({ dailyPuzzle }: GameProps) {
const speaker = hasWon ? '🔉' : '🔇';
const text = `Hördle #${dailyPuzzle.id}\n\n${speaker}${emojiGrid}\n\n#Hördle #Music\n\nhttps://hoerdle.elpatron.me`;
- navigator.clipboard.writeText(text).then(() => {
+ // Fallback method for copying to clipboard
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ try {
+ document.execCommand('copy');
setShareText('Copied!');
setTimeout(() => setShareText('Share Result'), 2000);
- });
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ setShareText('Copy failed');
+ setTimeout(() => setShareText('Share Result'), 2000);
+ } finally {
+ document.body.removeChild(textarea);
+ }
};
return (
@@ -123,6 +139,7 @@ export default function Game({ dailyPuzzle }: GameProps) {
You won!
Come back tomorrow for a new song.
+ {statistics &&
}
@@ -133,6 +150,7 @@ export default function Game({ dailyPuzzle }: GameProps) {
Game Over
The song was hidden.
+ {statistics &&
}
diff --git a/components/Statistics.tsx b/components/Statistics.tsx
new file mode 100644
index 0000000..8ad2b2e
--- /dev/null
+++ b/components/Statistics.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import { Statistics as StatsType } from '../lib/gameState';
+
+interface StatisticsProps {
+ statistics: StatsType;
+}
+
+const BADGES = {
+ 1: '🏆', // Gold trophy
+ 2: '🥈', // Silver medal
+ 3: '🥉', // Bronze medal
+ 4: '⭐', // Star
+ 5: '✨', // Sparkles
+ 6: '💫', // Dizzy
+ failed: '❌', // Cross mark
+};
+
+export default function Statistics({ statistics }: StatisticsProps) {
+ const total =
+ statistics.solvedIn1 +
+ statistics.solvedIn2 +
+ statistics.solvedIn3 +
+ statistics.solvedIn4 +
+ statistics.solvedIn5 +
+ statistics.solvedIn6 +
+ statistics.failed;
+
+ const stats = [
+ { attempts: 1, count: statistics.solvedIn1, badge: BADGES[1] },
+ { attempts: 2, count: statistics.solvedIn2, badge: BADGES[2] },
+ { attempts: 3, count: statistics.solvedIn3, badge: BADGES[3] },
+ { attempts: 4, count: statistics.solvedIn4, badge: BADGES[4] },
+ { attempts: 5, count: statistics.solvedIn5, badge: BADGES[5] },
+ { attempts: 6, count: statistics.solvedIn6, badge: BADGES[6] },
+ { attempts: 'Failed', count: statistics.failed, badge: BADGES.failed },
+ ];
+
+ return (
+
+
Your Statistics
+
Total puzzles: {total}
+
+ {stats.map((stat, index) => (
+
+
{stat.badge}
+
+ {typeof stat.attempts === 'number' ? `${stat.attempts} try` : stat.attempts}
+
+
{stat.count}
+
+ ))}
+
+
+ );
+}
diff --git a/lib/gameState.ts b/lib/gameState.ts
index 4354ca5..b705262 100644
--- a/lib/gameState.ts
+++ b/lib/gameState.ts
@@ -10,12 +10,25 @@ export interface GameState {
lastPlayed: number; // Timestamp
}
+export interface Statistics {
+ solvedIn1: number;
+ solvedIn2: number;
+ solvedIn3: number;
+ solvedIn4: number;
+ solvedIn5: number;
+ solvedIn6: number;
+ failed: number;
+}
+
const STORAGE_KEY = 'hoerdle_game_state';
+const STATS_KEY = 'hoerdle_statistics';
export function useGameState() {
const [gameState, setGameState] = useState
(null);
+ const [statistics, setStatistics] = useState(null);
useEffect(() => {
+ // Load game state
const stored = localStorage.getItem(STORAGE_KEY);
const today = new Date().toISOString().split('T')[0];
@@ -23,20 +36,48 @@ export function useGameState() {
const parsed: GameState = JSON.parse(stored);
if (parsed.date === today) {
setGameState(parsed);
- return;
+ } else {
+ // New day
+ const newState: GameState = {
+ date: today,
+ guesses: [],
+ isSolved: false,
+ isFailed: false,
+ lastPlayed: Date.now(),
+ };
+ setGameState(newState);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
}
+ } else {
+ // No state
+ const newState: GameState = {
+ date: today,
+ guesses: [],
+ isSolved: false,
+ isFailed: false,
+ lastPlayed: Date.now(),
+ };
+ setGameState(newState);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
}
- // New day or no state
- const newState: GameState = {
- date: today,
- guesses: [],
- isSolved: false,
- isFailed: false,
- lastPlayed: Date.now(),
- };
- setGameState(newState);
- localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
+ // Load statistics
+ const storedStats = localStorage.getItem(STATS_KEY);
+ if (storedStats) {
+ setStatistics(JSON.parse(storedStats));
+ } else {
+ const newStats: Statistics = {
+ solvedIn1: 0,
+ solvedIn2: 0,
+ solvedIn3: 0,
+ solvedIn4: 0,
+ solvedIn5: 0,
+ solvedIn6: 0,
+ failed: 0,
+ };
+ setStatistics(newStats);
+ localStorage.setItem(STATS_KEY, JSON.stringify(newStats));
+ }
}, []);
const saveState = (newState: GameState) => {
@@ -44,6 +85,28 @@ export function useGameState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
};
+ const updateStatistics = (attempts: number, solved: boolean) => {
+ if (!statistics) return;
+
+ const newStats = { ...statistics };
+
+ if (solved) {
+ switch (attempts) {
+ case 1: newStats.solvedIn1++; break;
+ case 2: newStats.solvedIn2++; break;
+ case 3: newStats.solvedIn3++; break;
+ case 4: newStats.solvedIn4++; break;
+ case 5: newStats.solvedIn5++; break;
+ case 6: newStats.solvedIn6++; break;
+ }
+ } else {
+ newStats.failed++;
+ }
+
+ setStatistics(newStats);
+ localStorage.setItem(STATS_KEY, JSON.stringify(newStats));
+ };
+
const addGuess = (guess: string, correct: boolean) => {
if (!gameState) return;
@@ -60,7 +123,12 @@ export function useGameState() {
};
saveState(newState);
+
+ // Update statistics when game ends
+ if (isSolved || isFailed) {
+ updateStatistics(newGuesses.length, isSolved);
+ }
};
- return { gameState, addGuess };
+ return { gameState, statistics, addGuess };
}