extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164import { browser } from '$app/environment';
export class GameNotifications { private originalTitle: string; private titleInterval: ReturnType<typeof setInterval> | null = null; private notificationPermission: NotificationPermission = 'default';
constructor() { if (browser) { this.originalTitle = document.title; // iOS Safari doesn't support the Notification API this.notificationPermission = ('Notification' in window) ? Notification.permission : 'denied'; } else { this.originalTitle = ''; } }
/** * Request notification permission from the user */ async requestPermission(): Promise<boolean> { if (!browser || !('Notification' in window)) { return false; }
if (Notification.permission === 'granted') { this.notificationPermission = 'granted'; return true; }
if (Notification.permission !== 'denied') { const permission = await Notification.requestPermission(); this.notificationPermission = permission; return permission === 'granted'; }
return false; }
/** * Start flashing the page title to get attention */ startTitleFlash(message: string) { if (!browser) return;
this.stopTitleFlash(); // Clear any existing flash this.originalTitle = document.title;
let isOriginal = true; this.titleInterval = setInterval(() => { document.title = isOriginal ? message : this.originalTitle; isOriginal = !isOriginal; }, 1000); }
/** * Stop flashing the title and restore the original */ stopTitleFlash() { if (!browser) return;
if (this.titleInterval) { clearInterval(this.titleInterval); this.titleInterval = null; } document.title = this.originalTitle; }
/** * Show a browser push notification */ async showNotification(title: string, options?: NotificationOptions) { if (!browser || !('Notification' in window)) { return; }
// Auto-request permission if not denied if (this.notificationPermission === 'default') { await this.requestPermission(); }
if (this.notificationPermission === 'granted') { try { const notification = new Notification(title, { icon: '/favicon.png', badge: '/favicon.png', ...options });
// Auto-close after 5 seconds setTimeout(() => notification.close(), 5000);
return notification; } catch (err) { console.error('Failed to show notification:', err); } } }
/** * Notify user of their turn with both title flash and push notification */ notifyYourTurn(opponentHandle?: string) { const message = opponentHandle ? `Your turn vs ${opponentHandle}!` : 'Your turn!';
this.startTitleFlash(`⚫ ${message}`); this.showNotification('Cloud Go - Your Turn', { body: message, tag: 'your-turn', requireInteraction: false }); }
/** * Notify user of a new move in the game they're watching */ notifyNewMove(playerHandle?: string) { const message = playerHandle ? `${playerHandle} played a move` : 'New move played';
this.startTitleFlash(`🔄 ${message}`); this.showNotification('Cloud Go - New Move', { body: message, tag: 'new-move', requireInteraction: false }); }
/** * Clean up when component unmounts */ cleanup() { this.stopTitleFlash(); }}
/** * Check if the page is currently visible/focused */export function isPageVisible(): boolean { if (!browser) return false; return document.visibilityState === 'visible';}
/** * Listen for visibility changes */export function onVisibilityChange(callback: (visible: boolean) => void): () => void { if (!browser) return () => {};
const handler = () => { callback(document.visibilityState === 'visible'); };
document.addEventListener('visibilitychange', handler);
return () => { document.removeEventListener('visibilitychange', handler); };}