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}