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 ( +
+
+

+ {t('title')} +

+ +
+ +

+ {t('message', { name })} +

+ + + {t('cta', { name })} + +
+ ); +} + + 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",