Feat: Auto-play on skip & Gotify notifications
This commit is contained in:
31
app/actions.ts
Normal file
31
app/actions.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
const GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
|
const GOTIFY_APP_TOKEN = process.env.GOTIFY_APP_TOKEN;
|
||||||
|
|
||||||
|
export async function sendGotifyNotification(attempts: number, status: 'won' | 'lost', puzzleId: number) {
|
||||||
|
try {
|
||||||
|
const title = `Hördle #${puzzleId} ${status === 'won' ? 'Solved!' : 'Failed'}`;
|
||||||
|
const message = status === 'won'
|
||||||
|
? `Puzzle #${puzzleId} was solved in ${attempts} attempt(s).`
|
||||||
|
: `Puzzle #${puzzleId} was failed after ${attempts} attempt(s).`;
|
||||||
|
|
||||||
|
const response = await fetch(`${GOTIFY_URL}/message?token=${GOTIFY_APP_TOKEN}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
priority: 5,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to send Gotify notification:', await response.text());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending Gotify notification:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@ interface AudioPlayerProps {
|
|||||||
src: string;
|
src: string;
|
||||||
unlockedSeconds: number; // 2, 4, 7, 11, 16, 30 (or full length)
|
unlockedSeconds: number; // 2, 4, 7, 11, 16, 30 (or full length)
|
||||||
onPlay?: () => void;
|
onPlay?: () => void;
|
||||||
|
autoPlay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AudioPlayer({ src, unlockedSeconds, onPlay }: AudioPlayerProps) {
|
export default function AudioPlayer({ src, unlockedSeconds, onPlay, autoPlay = false }: AudioPlayerProps) {
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
@@ -19,8 +20,23 @@ export default function AudioPlayer({ src, unlockedSeconds, onPlay }: AudioPlaye
|
|||||||
audioRef.current.currentTime = 0;
|
audioRef.current.currentTime = 0;
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
|
|
||||||
|
if (autoPlay) {
|
||||||
|
const playPromise = audioRef.current.play();
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise
|
||||||
|
.then(() => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
onPlay?.();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("Autoplay prevented:", error);
|
||||||
|
setIsPlaying(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [src, unlockedSeconds]);
|
}
|
||||||
|
}
|
||||||
|
}, [src, unlockedSeconds, autoPlay]);
|
||||||
|
|
||||||
const togglePlay = () => {
|
const togglePlay = () => {
|
||||||
if (!audioRef.current) return;
|
if (!audioRef.current) return;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AudioPlayer from './AudioPlayer';
|
|||||||
import GuessInput from './GuessInput';
|
import GuessInput from './GuessInput';
|
||||||
import Statistics from './Statistics';
|
import Statistics from './Statistics';
|
||||||
import { useGameState } from '../lib/gameState';
|
import { useGameState } from '../lib/gameState';
|
||||||
|
import { sendGotifyNotification } from '../app/actions';
|
||||||
|
|
||||||
interface GameProps {
|
interface GameProps {
|
||||||
dailyPuzzle: {
|
dailyPuzzle: {
|
||||||
@@ -24,6 +25,7 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
|||||||
const [hasWon, setHasWon] = useState(false);
|
const [hasWon, setHasWon] = useState(false);
|
||||||
const [hasLost, setHasLost] = useState(false);
|
const [hasLost, setHasLost] = useState(false);
|
||||||
const [shareText, setShareText] = useState('Share Result');
|
const [shareText, setShareText] = useState('Share Result');
|
||||||
|
const [lastAction, setLastAction] = useState<'GUESS' | 'SKIP' | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameState && dailyPuzzle) {
|
if (gameState && dailyPuzzle) {
|
||||||
@@ -32,6 +34,10 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
|||||||
}
|
}
|
||||||
}, [gameState, dailyPuzzle]);
|
}, [gameState, dailyPuzzle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLastAction(null);
|
||||||
|
}, [dailyPuzzle?.id]);
|
||||||
|
|
||||||
if (!dailyPuzzle) return (
|
if (!dailyPuzzle) return (
|
||||||
<div className="game-container" style={{ textAlign: 'center', padding: '2rem' }}>
|
<div className="game-container" style={{ textAlign: 'center', padding: '2rem' }}>
|
||||||
<h2>No Puzzle Available</h2>
|
<h2>No Puzzle Available</h2>
|
||||||
@@ -43,17 +49,32 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
|||||||
if (!gameState) return <div>Loading state...</div>;
|
if (!gameState) return <div>Loading state...</div>;
|
||||||
|
|
||||||
const handleGuess = (song: any) => {
|
const handleGuess = (song: any) => {
|
||||||
|
setLastAction('GUESS');
|
||||||
if (song.id === dailyPuzzle.songId) {
|
if (song.id === dailyPuzzle.songId) {
|
||||||
addGuess(song.title, true);
|
addGuess(song.title, true);
|
||||||
setHasWon(true);
|
setHasWon(true);
|
||||||
|
sendGotifyNotification(gameState.guesses.length + 1, 'won', dailyPuzzle.id);
|
||||||
} else {
|
} else {
|
||||||
addGuess(song.title, false);
|
addGuess(song.title, false);
|
||||||
if (gameState.guesses.length + 1 >= 7) {
|
if (gameState.guesses.length + 1 >= 7) {
|
||||||
setHasLost(true);
|
setHasLost(true);
|
||||||
|
sendGotifyNotification(7, 'lost', dailyPuzzle.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
setLastAction('SKIP');
|
||||||
|
addGuess("SKIPPED", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGiveUp = () => {
|
||||||
|
setLastAction('SKIP');
|
||||||
|
addGuess("SKIPPED", false);
|
||||||
|
setHasLost(true);
|
||||||
|
sendGotifyNotification(7, 'lost', dailyPuzzle.id);
|
||||||
|
};
|
||||||
|
|
||||||
const unlockedSeconds = UNLOCK_STEPS[Math.min(gameState.guesses.length, 6)];
|
const unlockedSeconds = UNLOCK_STEPS[Math.min(gameState.guesses.length, 6)];
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
@@ -116,6 +137,7 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
|||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
src={dailyPuzzle.audioUrl}
|
src={dailyPuzzle.audioUrl}
|
||||||
unlockedSeconds={unlockedSeconds}
|
unlockedSeconds={unlockedSeconds}
|
||||||
|
autoPlay={lastAction === 'SKIP'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -138,17 +160,14 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
|||||||
<GuessInput onGuess={handleGuess} disabled={false} />
|
<GuessInput onGuess={handleGuess} disabled={false} />
|
||||||
{gameState.guesses.length < 6 ? (
|
{gameState.guesses.length < 6 ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => addGuess("SKIPPED", false)}
|
onClick={handleSkip}
|
||||||
className="skip-button"
|
className="skip-button"
|
||||||
>
|
>
|
||||||
Skip (+{UNLOCK_STEPS[Math.min(gameState.guesses.length + 1, 6)] - unlockedSeconds}s)
|
Skip (+{UNLOCK_STEPS[Math.min(gameState.guesses.length + 1, 6)] - unlockedSeconds}s)
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={handleGiveUp}
|
||||||
addGuess("SKIPPED", false);
|
|
||||||
setHasLost(true);
|
|
||||||
}}
|
|
||||||
className="skip-button"
|
className="skip-button"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ services:
|
|||||||
- DATABASE_URL=file:/app/data/prod.db
|
- DATABASE_URL=file:/app/data/prod.db
|
||||||
- ADMIN_PASSWORD=admin123 # Change this!
|
- ADMIN_PASSWORD=admin123 # Change this!
|
||||||
- TZ=Europe/Berlin # Timezone for daily puzzle rotation
|
- TZ=Europe/Berlin # Timezone for daily puzzle rotation
|
||||||
|
- GOTIFY_URL=https://gotify.example.com
|
||||||
|
- GOTIFY_APP_TOKEN=your_gotify_token
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./public/uploads:/app/public/uploads
|
- ./public/uploads:/app/public/uploads
|
||||||
|
|||||||
Reference in New Issue
Block a user