diff --git a/app/[locale]/[genre]/page.tsx b/app/[locale]/[genre]/page.tsx
index f105ea8..b26d083 100644
--- a/app/[locale]/[genre]/page.tsx
+++ b/app/[locale]/[genre]/page.tsx
@@ -87,6 +87,9 @@ export default async function GenrePage({ params }: PageProps) {
return s.launchDate && s.launchDate > now;
});
+ // Required daily keys: global + all active genres (by localized name, as used in gameState storage)
+ const requiredDailyKeys = ['global', ...genres.map(g => getLocalizedValue(g.name, locale))];
+
return (
<>
@@ -156,7 +159,7 @@ export default async function GenrePage({ params }: PageProps) {
)}
-
+
>
);
}
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index e50aefa..28571ca 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -61,6 +61,9 @@ export default async function Home({
return s.launchDate && s.launchDate > now;
});
+ // Required daily keys: global + all active genres (by localized name, as used in gameState storage)
+ const requiredDailyKeys = ['global', ...genres.map(g => getLocalizedValue(g.name, locale))];
+
return (
<>
@@ -149,7 +152,7 @@ export default async function Home({
-
+
>
);
diff --git a/components/ExtraPuzzlesPopover.tsx b/components/ExtraPuzzlesPopover.tsx
new file mode 100644
index 0000000..8e023c0
--- /dev/null
+++ b/components/ExtraPuzzlesPopover.tsx
@@ -0,0 +1,98 @@
+'use client';
+
+import { useTranslations, useLocale } from 'next-intl';
+import type { ExternalPuzzle } from '@/lib/externalPuzzles';
+
+interface ExtraPuzzlesPopoverProps {
+ puzzle: ExternalPuzzle;
+ onClose: () => void;
+}
+
+export default function ExtraPuzzlesPopover({ puzzle, onClose }: ExtraPuzzlesPopoverProps) {
+ const t = useTranslations('ExtraPuzzles');
+ const locale = useLocale();
+
+ const name = locale === 'de' ? puzzle.nameDe : puzzle.nameEn;
+
+ const handleClick = () => {
+ if (typeof window !== 'undefined' && window.plausible) {
+ window.plausible('extra_puzzles_click', {
+ props: {
+ partner: puzzle.id,
+ url: puzzle.url,
+ },
+ });
+ }
+ onClose();
+ };
+
+ return (
+
+ );
+}
+
+
diff --git a/components/Game.tsx b/components/Game.tsx
index 7a51806..d01aa9e 100644
--- a/components/Game.tsx
+++ b/components/Game.tsx
@@ -6,7 +6,12 @@ import { useTranslations, useLocale } from 'next-intl';
import AudioPlayer, { AudioPlayerRef } from './AudioPlayer';
import GuessInput from './GuessInput';
import Statistics from './Statistics';
+import ExtraPuzzlesPopover from './ExtraPuzzlesPopover';
import { useGameState } from '../lib/gameState';
+import { getGenreKey } from '@/lib/playerStorage';
+import type { ExternalPuzzle } from '@/lib/externalPuzzles';
+import { getRandomExternalPuzzle } from '@/lib/externalPuzzles';
+import { hasPlayedAllDailyPuzzlesForToday, hasSeenExtraPuzzlesPopoverToday, markDailyPuzzlePlayedToday, markExtraPuzzlesPopoverShownToday } from '@/lib/extraPuzzlesTracker';
import { sendGotifyNotification, submitRating } from '../app/actions';
// Plausible Analytics
@@ -32,11 +37,14 @@ interface GameProps {
isSpecial?: boolean;
maxAttempts?: number;
unlockSteps?: number[];
+ // List of genre keys that zusammen alle Tagesrätsel des Tages repräsentieren (z. B. ['global', 'Rock', 'Pop']).
+ // Wird genutzt, um zu prüfen, ob der Spieler alle Tagesrätsel gespielt hat.
+ requiredDailyKeys?: string[];
}
const DEFAULT_UNLOCK_STEPS = [2, 4, 7, 11, 16, 30, 60];
-export default function Game({ dailyPuzzle, genre = null, isSpecial = false, maxAttempts = 7, unlockSteps = DEFAULT_UNLOCK_STEPS }: GameProps) {
+export default function Game({ dailyPuzzle, genre = null, isSpecial = false, maxAttempts = 7, unlockSteps = DEFAULT_UNLOCK_STEPS, requiredDailyKeys }: GameProps) {
const t = useTranslations('Game');
const locale = useLocale();
const { gameState, statistics, addGuess, giveUp, addReplay, addYearBonus, skipYearBonus } = useGameState(genre, maxAttempts, isSpecial);
@@ -49,6 +57,8 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
const [hasRated, setHasRated] = useState(false);
const [showYearModal, setShowYearModal] = useState(false);
const [hasPlayedAudio, setHasPlayedAudio] = useState(false);
+ const [showExtraPuzzlesPopover, setShowExtraPuzzlesPopover] = useState(false);
+ const [extraPuzzle, setExtraPuzzle] = useState(null);
const audioPlayerRef = useRef(null);
useEffect(() => {
@@ -81,6 +91,37 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
}
}, [gameState, dailyPuzzle]);
+ // Track gespielte Tagesrätsel & entscheide, ob das Partner-Popover gezeigt werden soll
+ useEffect(() => {
+ if (!gameState || !dailyPuzzle) return;
+
+ const gameEnded = gameState.isSolved || gameState.isFailed;
+ if (!gameEnded) return;
+
+ const genreKey = getGenreKey(isSpecial ? null : genre, isSpecial, isSpecial ? genre || undefined : undefined);
+ markDailyPuzzlePlayedToday(genreKey);
+
+ if (!requiredDailyKeys || requiredDailyKeys.length === 0) return;
+ if (hasSeenExtraPuzzlesPopoverToday()) return;
+ if (!hasPlayedAllDailyPuzzlesForToday(requiredDailyKeys)) return;
+
+ const partnerPuzzle = getRandomExternalPuzzle();
+ if (!partnerPuzzle) return;
+
+ setExtraPuzzle(partnerPuzzle);
+ setShowExtraPuzzlesPopover(true);
+ markExtraPuzzlesPopoverShownToday();
+
+ if (typeof window !== 'undefined' && window.plausible) {
+ window.plausible('extra_puzzles_popover_shown', {
+ props: {
+ partner: partnerPuzzle.id,
+ url: partnerPuzzle.url,
+ },
+ });
+ }
+ }, [gameState?.isSolved, gameState?.isFailed, dailyPuzzle?.id, genre, isSpecial, requiredDailyKeys]);
+
useEffect(() => {
setLastAction(null);
}, [dailyPuzzle?.id]);
@@ -490,6 +531,13 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
onSkip={handleYearSkip}
/>
)}
+
+ {showExtraPuzzlesPopover && extraPuzzle && (
+ setShowExtraPuzzlesPopover(false)}
+ />
+ )}
);
}
diff --git a/lib/externalPuzzles.ts b/lib/externalPuzzles.ts
new file mode 100644
index 0000000..46cab6c
--- /dev/null
+++ b/lib/externalPuzzles.ts
@@ -0,0 +1,47 @@
+export type ExternalPuzzle = {
+ id: string;
+ nameDe: string;
+ nameEn: string;
+ url: string;
+ isActive?: boolean;
+};
+
+/**
+ * Zentrale Liste externer Rätselangebote.
+ *
+ * Erweiterung: Einfach neuen Eintrag in dieses Array hinzufügen.
+ */
+export const externalPuzzles: ExternalPuzzle[] = [
+ {
+ id: 'pastpuzzle',
+ nameDe: 'Past Puzzle',
+ nameEn: 'Past Puzzle',
+ url: 'https://www.pastpuzzle.de/#/',
+ isActive: true,
+ },
+ {
+ id: 'woerdle',
+ nameDe: 'Wördle',
+ nameEn: 'Wördle',
+ url: 'https://www.wördle.de',
+ isActive: true,
+ },
+ {
+ id: 'ciddle',
+ nameDe: 'Ciddle',
+ nameEn: 'Ciddle',
+ url: 'https://ciddle.winklerweb.net',
+ isActive: true,
+ },
+];
+
+export function getRandomExternalPuzzle(): ExternalPuzzle | null {
+ const activePuzzles = externalPuzzles.filter(p => p.isActive !== false);
+ if (activePuzzles.length === 0) {
+ return null;
+ }
+ const index = Math.floor(Math.random() * activePuzzles.length);
+ return activePuzzles[index] ?? null;
+}
+
+
diff --git a/lib/extraPuzzlesTracker.ts b/lib/extraPuzzlesTracker.ts
new file mode 100644
index 0000000..e273e6b
--- /dev/null
+++ b/lib/extraPuzzlesTracker.ts
@@ -0,0 +1,68 @@
+import { getTodayISOString } from './dateUtils';
+
+const DAILY_PLAYED_PREFIX = 'hoerdle_daily_played_';
+const EXTRA_POPOVER_PREFIX = 'hoerdle_extra_puzzles_shown_';
+
+function getTodayKey(prefix: string): string | null {
+ if (typeof window === 'undefined') return null;
+ const today = getTodayISOString();
+ return `${prefix}${today}`;
+}
+
+export function markDailyPuzzlePlayedToday(genreKey: string) {
+ const storageKey = getTodayKey(DAILY_PLAYED_PREFIX);
+ if (!storageKey) return;
+
+ try {
+ const raw = window.localStorage.getItem(storageKey);
+ const list: string[] = raw ? JSON.parse(raw) : [];
+ if (!list.includes(genreKey)) {
+ list.push(genreKey);
+ window.localStorage.setItem(storageKey, JSON.stringify(list));
+ }
+ } catch (e) {
+ console.warn('[extraPuzzles] Failed to mark daily puzzle as played', e);
+ }
+}
+
+export function hasPlayedAllDailyPuzzlesForToday(requiredGenreKeys: string[]): boolean {
+ const storageKey = getTodayKey(DAILY_PLAYED_PREFIX);
+ if (!storageKey) return false;
+
+ try {
+ const raw = window.localStorage.getItem(storageKey);
+ const played: string[] = raw ? JSON.parse(raw) : [];
+ if (!Array.isArray(played) || played.length === 0) {
+ return false;
+ }
+ return requiredGenreKeys.every(key => played.includes(key));
+ } catch (e) {
+ console.warn('[extraPuzzles] Failed to read played puzzles', e);
+ return false;
+ }
+}
+
+export function hasSeenExtraPuzzlesPopoverToday(): boolean {
+ const storageKey = getTodayKey(EXTRA_POPOVER_PREFIX);
+ if (!storageKey) return false;
+
+ try {
+ return window.localStorage.getItem(storageKey) === 'true';
+ } catch (e) {
+ console.warn('[extraPuzzles] Failed to read popover state', e);
+ return false;
+ }
+}
+
+export function markExtraPuzzlesPopoverShownToday() {
+ const storageKey = getTodayKey(EXTRA_POPOVER_PREFIX);
+ if (!storageKey) return;
+
+ try {
+ window.localStorage.setItem(storageKey, 'true');
+ } catch (e) {
+ console.warn('[extraPuzzles] Failed to persist popover state', e);
+ }
+}
+
+
diff --git a/messages/de.json b/messages/de.json
index c7e9605..cb2707e 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -64,6 +64,12 @@
"special": "Special",
"genre": "Genre"
},
+ "ExtraPuzzles": {
+ "title": "Noch nicht genug Rätsel?",
+ "message": "Hey, hast du Lust auf weitere Rätsel? Dann schau doch mal bei {name} vorbei!",
+ "cta": "Zu {name}",
+ "close": "Schließen"
+ },
"Statistics": {
"yourStatistics": "Deine Statistiken",
"totalPuzzles": "Gesamte Rätsel",
diff --git a/messages/en.json b/messages/en.json
index c68a43e..d951d2d 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -64,6 +64,12 @@
"special": "Special",
"genre": "Genre"
},
+ "ExtraPuzzles": {
+ "title": "Still in the mood for puzzles?",
+ "message": "Hey, would you like to try some more puzzles? Then take a look at {name}!",
+ "cta": "Go to {name}",
+ "close": "Close"
+ },
"Statistics": {
"yourStatistics": "Your Statistics",
"totalPuzzles": "Total puzzles",