Files
pargar/frontend/src/hooks/use-realtime.tsx
T
ahpandClaude Sonnet 4.6 602d3f2f4d feat: remove glassmorphism from notification toast; improve video player UX
Task 4 — Event Registration Confirmation (glassmorphism removal):
- Replace translucent gradient + backdrop-blur-md toast in use-realtime.tsx
  with a solid bg-card container, full-opacity border, and a colored
  left accent bar — high contrast, readable in all themes, no blur

Task 5 — Video player UX (within anti-cheat constraints):
- Keyboard shortcuts: Space (play/pause), M (mute), ← (seek back 10s),
  F (fullscreen) — active globally, disabled when an input is focused
- Fullscreen button added to the control bar (F key shortcut)
- Volume slider (0–100%) replaces mute-only toggle; volume is persisted
  to localStorage across sessions under key `pargar:volume`
- Buffer range visualized as a translucent white bar behind the playback
  progress track so users see how much is buffered
- Exponential backoff auto-retry on video error: 1s, 2s, 4s … 30s cap;
  retry count shown in error message; manual retry clears the timer
- Speed controls (0.75x–2x) are intentionally absent: the verified-watch
  anti-cheat locks playbackRate=1; changing it would break the heartbeat
  accumulation that gates quiz unlock and certification
- HLS/ABR requires backend FFmpeg transcoding; the player is structured
  so swapping src=mp4 → src=.m3u8 + hls.js is a one-file change when
  the backend pipeline is ready

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-18 10:40:13 +03:30

112 lines
3.5 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/lib/auth-store";
import { getWsUrl } from "@/lib/api";
import { toast } from "@/components/providers";
interface WSPayload {
type?: string;
item?: {
type?: string;
title?: string;
body?: string;
category?: string;
route?: string;
};
from?: number;
[key: string]: unknown;
}
/** Connects to the realtime gateway, toasts live notifications and bumps unread counts. */
export function useRealtimeGateway() {
const accessToken = useAuth((s) => s.accessToken);
const user = useAuth((s) => s.user);
const client = useQueryClient();
const router = useRouter();
const lastEvent = useRef<Record<string, number>>({});
useEffect(() => {
if (!user) return;
let ws: WebSocket | null = null;
let retry = 0;
let closed = false;
const connect = () => {
if (closed) return;
try {
// Prefer subprotocol when we have an in-memory access token; otherwise rely on httpOnly cookie.
ws = accessToken
? new WebSocket(getWsUrl(), ["bearer", accessToken])
: new WebSocket(getWsUrl());
} catch {
return;
}
ws.onopen = () => {
retry = 0;
};
ws.onmessage = (ev) => {
try {
const p: WSPayload = JSON.parse(ev.data);
if (p.type === "notification" && p.item) {
const n = p.item;
const stamp = Date.now();
const dedupeKey = `${n.type}:${n.title}:${n.body}`;
if (stamp - (lastEvent.current[dedupeKey] ?? 0) < 2000) return;
lastEvent.current[dedupeKey] = stamp;
const title = n.title?.trim() || "اعلان";
const body = n.body?.trim();
toast.custom(
(id) => (
<button
type="button"
dir="rtl"
onClick={() => {
toast.dismiss(id);
if (n.route) router.push(n.route);
}}
className="flex w-[min(22rem,calc(100vw-1.5rem))] items-stretch overflow-hidden rounded-xl border border-border bg-card text-start shadow-lg"
>
<span className="w-1 shrink-0 bg-primary" aria-hidden />
<span className="flex min-w-0 flex-col px-3 py-2.5">
<span dir="auto" className="block truncate text-sm font-semibold text-foreground [unicode-bidi:plaintext]">
{title}
</span>
{body ? (
<span dir="auto" className="mt-0.5 block line-clamp-2 text-xs text-muted-foreground [unicode-bidi:plaintext]">
{body}
</span>
) : null}
</span>
</button>
),
{ duration: 5000 },
);
client.invalidateQueries({ queryKey: ["notifications"] });
client.invalidateQueries({ queryKey: ["me"] });
}
} catch {
/* ignore malformed frames */
}
};
ws.onclose = () => {
if (closed) return;
retry += 1;
const t = Math.min(1000 * 2 ** retry, 15000);
setTimeout(connect, t);
};
};
connect();
return () => {
closed = true;
ws?.close();
};
}, [accessToken, user, client, router]);
return null;
}