diff --git a/src/app/quiz/[quizId]/client.tsx b/src/app/quiz/[quizId]/client.tsx index 5e7150dc..892e7d4f 100644 --- a/src/app/quiz/[quizId]/client.tsx +++ b/src/app/quiz/[quizId]/client.tsx @@ -249,10 +249,17 @@ function QuizPageContent({ quizId }: { quizId: string }): React.JSX.Element { quizie. Jak to działa? - Po włączeniu tego trybu, jeśli opuścisz tę kartę lub nie - wykonasz żadnej akcji przez 5 minut, timer zostanie - automatycznie zatrzymany, a aplikacja odtworzy głośny dźwięk i - pokaże powiadomienie przypominające o powrocie do nauki. + Po włączeniu tego trybu, jeśli opuścisz tę kartę, przełączysz + się do innego okna lub aplikacji albo nie wykonasz żadnej akcji + przez 5 minut, timer zostanie automatycznie zatrzymany, a + aplikacja odtworzy głośny dźwięk i pokaże powiadomienie + przypominające o powrocie do nauki. + + + Na niektórych urządzeniach i przeglądarkach, zwłaszcza na + iPhonie i iPadzie, system może zablokować automatyczne + odtworzenie dźwięku. Powiadomienie nadal się pokaże, ale dźwięk + trybu skupienia może nie zawsze zadziałać. diff --git a/src/components/quiz/hooks/use-focus-mode.ts b/src/components/quiz/hooks/use-focus-mode.ts index 23512af1..0bda9ae6 100644 --- a/src/components/quiz/hooks/use-focus-mode.ts +++ b/src/components/quiz/hooks/use-focus-mode.ts @@ -10,9 +10,10 @@ const FOCUS_ALERT_CONTENT = { message: "Minęło 5 minut bez żadnej akcji. Timer został zatrzymany - wróć do nauki!", }, - tabLeft: { - title: "Opuszczono kartę z quizem.", - message: "Timer został zatrzymany - skup się na nauce!", + focusLeft: { + title: "Opuszczono quiz.", + message: + "Timer został zatrzymany - wróć do okna z quizem, żeby kontynuować naukę.", }, } as const; @@ -59,6 +60,23 @@ function playFocusAlertSound(audioRef: { current: HTMLAudioElement | null }) { void audio.play().catch(console.error); } +function triggerFocusAlert( + type: FocusAlertType, + timerStore: TimerStore, + isAlertOpenRef: { current: boolean }, + audioRef: { current: HTMLAudioElement | null }, + showFocusAlert: (type: FocusAlertType) => void, +) { + if (isAlertOpenRef.current) { + return; + } + + isAlertOpenRef.current = true; + timerStore.pause(); + playFocusAlertSound(audioRef); + showFocusAlert(type); +} + function startInactivityCountdown( timerStore: TimerStore, inactivityTimerRef: { current: NodeJS.Timeout | null }, @@ -70,13 +88,13 @@ function startInactivityCountdown( inactivityTimerRef.current = setTimeout( () => { - if (isAlertOpenRef.current) { - return; - } - isAlertOpenRef.current = true; - timerStore.pause(); - playFocusAlertSound(audioRef); - showFocusAlert("inactivity"); + triggerFocusAlert( + "inactivity", + timerStore, + isAlertOpenRef, + audioRef, + showFocusAlert, + ); }, 5 * 60 * 1000, ); @@ -176,22 +194,28 @@ export function useFocusMode(timerStore: TimerStore) { showFocusAlert, ); + const handleFocusLoss = () => { + triggerFocusAlert( + "focusLeft", + timerStore, + isAlertOpenRef, + audioRef, + showFocusAlert, + ); + }; + const handleVisibilityChange = () => { if (document.hidden) { - if (isAlertOpenRef.current) { - return; - } - isAlertOpenRef.current = true; - timerStore.pause(); - playFocusAlertSound(audioRef); - showFocusAlert("tabLeft"); + handleFocusLoss(); } }; document.addEventListener("visibilitychange", handleVisibilityChange); + window.addEventListener("blur", handleFocusLoss); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); + window.removeEventListener("blur", handleFocusLoss); clearInactivityCountdown(inactivityTimerRef); }; }, [isFocusModeActive, timerStore]); diff --git a/src/components/quiz/question-card.tsx b/src/components/quiz/question-card.tsx index 71a269e7..5351cc5f 100644 --- a/src/components/quiz/question-card.tsx +++ b/src/components/quiz/question-card.tsx @@ -163,7 +163,7 @@ export function QuestionCard({ aria-label="Otwórz repozytorium Testownik na GitHubie i zostaw gwiazdkę" > - Wesprzyj nas gwiazdką + Wbij nam gwiazdkę )} className="group w-full" diff --git a/src/components/quiz/quiz-info-card.tsx b/src/components/quiz/quiz-info-card.tsx index 502be937..3712af4a 100644 --- a/src/components/quiz/quiz-info-card.tsx +++ b/src/components/quiz/quiz-info-card.tsx @@ -20,6 +20,7 @@ import { toast } from "sonner"; import { AppContext } from "@/app-context"; import { QuizSettingsDialog } from "@/components/quiz/quiz-settings-dialog"; import { ShareQuizDialog } from "@/components/quiz/share-quiz-dialog/share-quiz-dialog"; +import { Badge } from "@/components/ui/badge"; import { Button, ButtonLink } from "@/components/ui/button"; import { Card, @@ -169,7 +170,15 @@ export function QuizInfoCard({ <> - {quiz.title} + + {quiz.title} + {isFocusModeActive ? ( + + + Tryb skupienia + + ) : null} + {quiz.creator == null ? null : ( by {quiz.creator.full_name} )} diff --git a/src/components/report-bug-dialog.tsx b/src/components/report-bug-dialog.tsx index 8c724559..9d1841f4 100644 --- a/src/components/report-bug-dialog.tsx +++ b/src/components/report-bug-dialog.tsx @@ -37,6 +37,8 @@ interface ReportBugDialogProps { onOpenChange: (open: boolean) => void; } +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const DEFAULT_FORM_STATE = { name: "", email: "", @@ -71,6 +73,9 @@ export function ReportBugDialog({ open, onOpenChange }: ReportBugDialogProps) { if (!form.name.trim()) { errors.name = "Podaj swoje imię."; } + if (form.email.trim() && !EMAIL_REGEX.test(form.email.trim())) { + errors.email = "Podaj poprawny adres e-mail."; + } if (!form.title.trim()) { errors.title = "Podaj tytuł zgłoszenia."; } @@ -202,11 +207,16 @@ export function ReportBugDialog({ open, onOpenChange }: ReportBugDialogProps) { + {formErrors.email ? ( +

{formErrors.email}

+ ) : null}