Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/app/quiz/[quizId]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,17 @@ function QuizPageContent({ quizId }: { quizId: string }): React.JSX.Element {
quizie.
<span className="mt-2 block">
<strong>Jak to działa? </strong>
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.
</span>
<span className="mt-2 block text-sm">
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ć.
</span>
</AlertDialogDescription>
</AlertDialogHeader>
Expand Down
58 changes: 41 additions & 17 deletions src/components/quiz/hooks/use-focus-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 },
Expand All @@ -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,
);
Expand Down Expand Up @@ -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]);
Expand Down
2 changes: 1 addition & 1 deletion src/components/quiz/question-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function QuestionCard({
aria-label="Otwórz repozytorium Testownik na GitHubie i zostaw gwiazdkę"
>
<SiGithub className="size-4" />
Wesprzyj nas gwiazdką
Wbij nam gwiazdkę
</Link>
)}
className="group w-full"
Expand Down
11 changes: 10 additions & 1 deletion src/components/quiz/quiz-info-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -169,7 +170,15 @@ export function QuizInfoCard({
<>
<Card>
<CardHeader>
<CardTitle>{quiz.title}</CardTitle>
<CardTitle className="flex flex-wrap items-center gap-2 pr-2">
<span className="min-w-0 break-words">{quiz.title}</span>
{isFocusModeActive ? (
<Badge className="bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
<ScanEyeIcon />
Tryb skupienia
</Badge>
) : null}
</CardTitle>
{quiz.creator == null ? null : (
<CardDescription>by {quiz.creator.full_name}</CardDescription>
)}
Expand Down
10 changes: 10 additions & 0 deletions src/components/report-bug-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ interface ReportBugDialogProps {
onOpenChange: (open: boolean) => void;
}

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const DEFAULT_FORM_STATE = {
name: "",
email: "",
Expand Down Expand Up @@ -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.";
}
Expand Down Expand Up @@ -202,11 +207,16 @@ export function ReportBugDialog({ open, onOpenChange }: ReportBugDialogProps) {
</Label>
<Input
id="email"
type="email"
disabled={isSending}
placeholder="jan.kowalski@solvro.pl"
value={form.email}
onChange={handleChange}
aria-invalid={Boolean(formErrors.email)}
/>
{formErrors.email ? (
<p className="text-destructive text-xs">{formErrors.email}</p>
) : null}
</div>
</div>
<div className="space-y-2">
Expand Down
Loading