Function bodies 62 total
shuffleArray function · typescript · L32-L39 (8 LOC)party/server.ts
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}constructor method · typescript · L44-L62 (19 LOC)party/server.ts
constructor(readonly room: Party.Room) {
this.state = {
roomCode: room.id,
hostId: "",
phase: "lobby",
players: [],
maxPlayers: MAX_PLAYERS,
questionIds: [],
currentQuestionIndex: 0,
currentTurnPlayerId: null,
questionsPerRound: 10,
selectedAnswer: null,
showExplanation: false,
turnDeadline: null,
turnTimeoutSeconds: TURN_TIMEOUT_SECONDS,
lobbyDisconnectTimes: {},
alarmPurpose: null,
};
}send method · typescript · L66-L68 (3 LOC)party/server.ts
send(connection: Party.Connection, msg: ServerMessage) {
connection.send(JSON.stringify(msg));
}broadcast method · typescript · L70-L72 (3 LOC)party/server.ts
broadcast(msg: ServerMessage, without?: string[]) {
this.room.broadcast(JSON.stringify(msg), without);
}getSnapshot method · typescript · L74-L90 (17 LOC)party/server.ts
getSnapshot(): RoomStateSnapshot {
return {
roomCode: this.state.roomCode,
hostId: this.state.hostId,
phase: this.state.phase,
players: this.state.players,
maxPlayers: this.state.maxPlayers,
questionIds: this.state.questionIds,
currentQuestionIndex: this.state.currentQuestionIndex,
currentTurnPlayerId: this.state.currentTurnPlayerId,
questionsPerRound: this.state.questionsPerRound,
selectedAnswer: this.state.selectedAnswer,
showExplanation: this.state.showExplanation,
turnDeadline: this.state.turnDeadline,
turnTimeoutSeconds: this.state.turnTimeoutSeconds,
};
}scheduleAlarm method · typescript · L92-L95 (4 LOC)party/server.ts
scheduleAlarm(ms: number, purpose: RoomState['alarmPurpose']) {
this.state.alarmPurpose = purpose;
this.room.storage.setAlarm(Date.now() + ms);
}getConnectedPlayers method · typescript · L97-L99 (3 LOC)party/server.ts
getConnectedPlayers(): OnlinePlayer[] {
return this.state.players.filter(p => p.connected);
}Repobility analyzer · published findings · https://repobility.com
getNextTurnPlayerId method · typescript · L101-L110 (10 LOC)party/server.ts
getNextTurnPlayerId(): string | null {
const connected = this.getConnectedPlayers();
if (connected.length === 0) return null;
if (!this.state.currentTurnPlayerId) return connected[0].id;
const currentIdx = connected.findIndex(p => p.id === this.state.currentTurnPlayerId);
const nextIdx = (currentIdx + 1) % connected.length;
return connected[nextIdx].id;
}getCurrentQuestionId method · typescript · L112-L115 (4 LOC)party/server.ts
getCurrentQuestionId(): number | null {
if (this.state.currentQuestionIndex >= this.state.questionIds.length) return null;
return this.state.questionIds[this.state.currentQuestionIndex];
}broadcastPresenceCount method · typescript · L123-L126 (4 LOC)party/server.ts
broadcastPresenceCount() {
const count = [...this.room.getConnections()].length;
this.room.broadcast(JSON.stringify({ type: "presence-count", count }));
}onConnect method · typescript · L130-L151 (22 LOC)party/server.ts
onConnect(connection: Party.Connection, ctx: Party.ConnectionContext) {
if (this.isPresenceRoom) {
const count = [...this.room.getConnections()].length;
this.send(connection, { type: "presence-count" as any, count } as any);
this.broadcastPresenceCount();
return;
}
// Check if this is a reconnecting player
const existing = this.state.players.find(p => p.id === connection.id);
if (existing) {
existing.connected = true;
delete this.state.lobbyDisconnectTimes[connection.id];
this.send(connection, { type: "room-state", state: this.getSnapshot() });
this.broadcast({ type: "player-reconnected", playerId: connection.id }, [connection.id]);
return;
}
// New connection — don't add to players yet, wait for 'join' message
// But send current state so client knows the room phase
this.send(connection, { type: "room-state", state: this.getSnapshot() });
}onMessage method · typescript · L153-L187 (35 LOC)party/server.ts
onMessage(message: string | ArrayBuffer | ArrayBufferView, sender: Party.Connection) {
if (this.isPresenceRoom) return;
if (typeof message !== "string") return;
let msg: ClientMessage;
try {
msg = JSON.parse(message);
} catch {
return;
}
switch (msg.type) {
case "join":
this.handleJoin(sender, msg.name);
break;
case "start-game":
this.handleStartGame(sender);
break;
case "answer":
this.handleAnswer(sender, msg.letter, msg.lang);
break;
case "next-question":
this.handleNextQuestion(sender);
break;
case "set-question-count":
this.handleSetQuestionCount(sender, msg.count);
break;
case "restart-game":
this.handleRestartGame(sender);
break;
case "update-name":
this.handleUpdateName(sender, msg.name);
break;
}
}onClose method · typescript · L189-L227 (39 LOC)party/server.ts
onClose(connection: Party.Connection) {
if (this.isPresenceRoom) {
this.broadcastPresenceCount();
return;
}
const player = this.state.players.find(p => p.id === connection.id);
if (!player) return;
player.connected = false;
this.broadcast({ type: "player-disconnected", playerId: connection.id });
// In lobby, start grace period before removing the player
if (this.state.phase === "lobby") {
this.state.lobbyDisconnectTimes[connection.id] = Date.now();
if (this.state.alarmPurpose !== "lobby-remove") {
this.scheduleAlarm(LOBBY_DISCONNECT_GRACE_MS, "lobby-remove");
}
}
// If host left, promote next connected player
if (connection.id === this.state.hostId) {
const nextHost = this.getConnectedPlayers()[0];
if (nextHost) {
this.state.hostId = nextHost.id;
this.broadcast({ type: "host-changed", newHostId: nextHost.id });
}
}
// If it was their turn during playing, sonAlarm method · typescript · L229-L248 (20 LOC)party/server.ts
onAlarm() {
switch (this.state.alarmPurpose) {
case "turn-timeout":
this.handleTurnTimeout();
break;
case "auto-advance":
this.advanceToNextQuestion();
break;
case "lobby-remove":
this.handleLobbyRemove();
break;
case "room-expiry":
// Close all connections if room expired
for (const conn of this.room.getConnections()) {
conn.close(1000, "Room expired");
}
break;
}
this.state.alarmPurpose = null;
}handleJoin method · typescript · L252-L297 (46 LOC)party/server.ts
handleJoin(sender: Party.Connection, name: string) {
// Reconnection: player already in the room (stable ID matched)
const existing = this.state.players.find(p => p.id === sender.id);
if (existing) {
const trimmedName = name.trim().slice(0, 20);
if (trimmedName && trimmedName !== existing.name) {
existing.name = trimmedName;
this.broadcast({ type: "room-state", state: this.getSnapshot() });
}
return;
}
// New player — only allow joining in lobby phase
if (this.state.phase !== "lobby") {
this.send(sender, { type: "error", message: "gameInProgress" });
return;
}
if (this.state.players.filter(p => p.connected).length >= this.state.maxPlayers) {
this.send(sender, { type: "error", message: "roomFull" });
return;
}
const trimmedName = name.trim().slice(0, 20) || `Player ${this.state.players.length + 1}`;
const player: OnlinePlayer = {
id: sender.id,
name: trimmedName,
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
handleUpdateName method · typescript · L299-L308 (10 LOC)party/server.ts
handleUpdateName(sender: Party.Connection, name: string) {
const player = this.state.players.find(p => p.id === sender.id);
if (!player) return;
const trimmedName = name.trim().slice(0, 20);
if (!trimmedName || trimmedName === player.name) return;
player.name = trimmedName;
this.broadcast({ type: "room-state", state: this.getSnapshot() });
}handleStartGame method · typescript · L310-L351 (42 LOC)party/server.ts
handleStartGame(sender: Party.Connection) {
if (sender.id !== this.state.hostId) {
this.send(sender, { type: "error", message: "notHost" });
return;
}
if (this.state.phase !== "lobby") return;
// Clean up disconnected players before starting
this.state.players = this.state.players.filter(p => p.connected);
this.state.lobbyDisconnectTimes = {};
const connectedCount = this.getConnectedPlayers().length;
if (connectedCount < 2) {
this.send(sender, { type: "error", message: "needMorePlayers" });
return;
}
// Shuffle all 100 question IDs and take questionsPerRound * number of players
// (each question is answered by one player, round-robin)
const allIds = answerKeys.map(q => q.id);
const shuffled = shuffleArray(allIds);
const totalQuestions = this.state.questionsPerRound * connectedCount;
this.state.questionIds = shuffled.slice(0, Math.min(totalQuestions, shuffled.length));
// Reset scores
for (handleAnswer method · typescript · L353-L391 (39 LOC)party/server.ts
handleAnswer(sender: Party.Connection, letter: string, lang: 'ar' | 'en') {
if (this.state.phase !== "playing") return;
if (sender.id !== this.state.currentTurnPlayerId) return;
if (this.state.selectedAnswer !== null) return; // Already answered
const questionId = this.getCurrentQuestionId();
if (questionId === null) return;
const answerKey = answerKeys.find(a => a.id === questionId);
if (!answerKey) return;
const correctAnswer = lang === 'ar' ? answerKey.arAns : answerKey.enAns;
const isCorrect = letter === correctAnswer;
this.state.selectedAnswer = letter;
this.state.showExplanation = true;
if (isCorrect) {
const player = this.state.players.find(p => p.id === sender.id);
if (player) player.score += 1;
}
// Build scores map
const scores: Record<string, number> = {};
for (const p of this.state.players) {
scores[p.id] = p.score;
}
this.broadcast({
type: "answer-revealed",
lettehandleNextQuestion method · typescript · L393-L399 (7 LOC)party/server.ts
handleNextQuestion(sender: Party.Connection) {
// Allow host or current turn player to advance
if (sender.id !== this.state.hostId && sender.id !== this.state.currentTurnPlayerId) return;
if (!this.state.showExplanation) return;
this.advanceToNextQuestion();
}handleSetQuestionCount method · typescript · L401-L410 (10 LOC)party/server.ts
handleSetQuestionCount(sender: Party.Connection, count: number) {
if (sender.id !== this.state.hostId) return;
if (this.state.phase !== "lobby") return;
const clamped = Math.max(5, Math.min(25, count));
this.state.questionsPerRound = clamped;
// Broadcast updated state
this.broadcast({ type: "room-state", state: this.getSnapshot() });
}handleTurnTimeout method · typescript · L412-L427 (16 LOC)party/server.ts
handleTurnTimeout() {
if (this.state.phase !== "playing") return;
if (this.state.showExplanation) return;
const playerId = this.state.currentTurnPlayerId;
if (!playerId) return;
// Score 0 for timeout — mark answer as null, show explanation
this.state.selectedAnswer = null;
this.state.showExplanation = true;
this.broadcast({ type: "turn-timeout", playerId });
// Auto-advance after brief pause
this.scheduleAlarm(3000, "auto-advance");
}handleLobbyRemove method · typescript · L429-L468 (40 LOC)party/server.ts
handleLobbyRemove() {
if (this.state.phase !== "lobby") return;
const now = Date.now();
const toRemove: string[] = [];
let nextRemovalMs = Infinity;
for (const [playerId, disconnectTime] of Object.entries(this.state.lobbyDisconnectTimes)) {
const elapsed = now - disconnectTime;
if (elapsed >= LOBBY_DISCONNECT_GRACE_MS) {
toRemove.push(playerId);
} else {
nextRemovalMs = Math.min(nextRemovalMs, LOBBY_DISCONNECT_GRACE_MS - elapsed);
}
}
for (const playerId of toRemove) {
this.state.players = this.state.players.filter(p => p.id !== playerId);
delete this.state.lobbyDisconnectTimes[playerId];
this.broadcast({ type: "player-left", playerId });
}
// If host was removed, promote next connected player
if (toRemove.includes(this.state.hostId)) {
const nextHost = this.getConnectedPlayers()[0];
if (nextHost) {
this.state.hostId = nextHost.id;
this.broadcast({ type: "handleRestartGame method · typescript · L470-L496 (27 LOC)party/server.ts
handleRestartGame(sender: Party.Connection) {
if (sender.id !== this.state.hostId) {
this.send(sender, { type: "error", message: "notHost" });
return;
}
if (this.state.phase !== "finished") return;
this.state.phase = "lobby";
this.state.questionIds = [];
this.state.currentQuestionIndex = 0;
this.state.currentTurnPlayerId = null;
this.state.selectedAnswer = null;
this.state.showExplanation = false;
this.state.turnDeadline = null;
this.state.alarmPurpose = null;
for (const p of this.state.players) {
p.score = 0;
}
// Remove disconnected players
this.state.players = this.state.players.filter(p => p.connected);
this.state.lobbyDisconnectTimes = {};
this.broadcast({ type: "room-state", state: this.getSnapshot() });
this.scheduleAlarm(ROOM_EXPIRY_LOBBY_MS, "room-expiry");
}All rows above produced by Repobility · https://repobility.com
advanceToNextQuestion method · typescript · L498-L533 (36 LOC)party/server.ts
advanceToNextQuestion() {
if (this.state.phase !== "playing") return;
this.state.currentQuestionIndex += 1;
// Check if game is over
if (this.state.currentQuestionIndex >= this.state.questionIds.length) {
this.state.phase = "finished";
this.state.currentTurnPlayerId = null;
this.state.turnDeadline = null;
const rankings = [...this.state.players]
.sort((a, b) => b.score - a.score)
.map(p => ({ id: p.id, name: p.name, score: p.score }));
this.broadcast({ type: "game-over", rankings });
this.scheduleAlarm(ROOM_EXPIRY_FINISHED_MS, "room-expiry");
return;
}
// Next turn
this.state.currentTurnPlayerId = this.getNextTurnPlayerId();
this.state.selectedAnswer = null;
this.state.showExplanation = false;
const deadline = Date.now() + this.state.turnTimeoutSeconds * 1000;
this.state.turnDeadline = deadline;
this.scheduleAlarm(this.state.turnTimeoutSeconds * 1000, "turn-timeout");
OnlineGame function · typescript · L23-L42 (20 LOC)src/App.tsx
function OnlineGame({ onBack }: { onBack: () => void }) {
const { screen, disconnect } = useOnlineStore();
const handleBack = () => {
disconnect();
onBack();
};
switch (screen) {
case 'lobby':
return <LobbyScreen onBack={handleBack} />;
case 'playing':
return <OnlinePlayingScreen />;
case 'finished':
return <OnlineGameOverScreen onBack={onBack} />;
case 'setup':
default:
return <OnlineSetupScreen onBack={handleBack} />;
}
}App function · typescript · L44-L93 (50 LOC)src/App.tsx
export default function App() {
const [appMode, setAppMode] = useState<AppMode>(() => {
const params = new URLSearchParams(window.location.search);
return params.get('room') ? 'online' : 'menu';
});
const { lang, gameScreen, currentTurn, setGameScreen } = useGameStore();
const { screen: onlineScreen, players, currentTurnPlayerId } = useOnlineStore();
useWakeLock(appMode === 'local' && gameScreen === 'playing');
const isRTL = lang === 'ar';
const onlinePlayerIndex = players.findIndex(p => p.id === currentTurnPlayerId);
const currentTheme =
appMode === 'local' && gameScreen === 'playing'
? PLAYER_THEMES[currentTurn]
: appMode === 'online' && onlineScreen === 'playing'
? PLAYER_THEMES[onlinePlayerIndex % PLAYER_THEMES.length] || PLAYER_THEMES[0]
: PLAYER_THEMES[0];
return (
<main dir={isRTL ? 'rtl' : 'ltr'} className={`min-h-screen transition-colors duration-500 font-sans flex flex-col items-center ${currentTheme}`}>
{aErrorBoundary class · typescript · L12-L45 (34 LOC)src/components/ErrorBoundary.tsx
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('ErrorBoundary caught:', error, info);
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-teal-50">
<div className="text-center p-8 bg-white/80 backdrop-blur-md rounded-3xl shadow-xl max-w-sm">
<h1 className="text-2xl font-bold text-teal-800 mb-4">Something went wrong</h1>
<p className="text-gray-600 mb-6">An unexpected error occurred.</p>
<button
onClick={() => {
this.setState({ hasError: false });
window.location.reload();
}}
className="px-6 py-3 bg-teal-600 hover:bg-teal-700 text-white rounded-xl font-bold transition-all"
getDerivedStateFromError method · typescript · L15-L17 (3 LOC)src/components/ErrorBoundary.tsx
static getDerivedStateFromError(): State {
return { hasError: true };
}componentDidCatch method · typescript · L19-L21 (3 LOC)src/components/ErrorBoundary.tsx
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('ErrorBoundary caught:', error, info);
}render method · typescript · L23-L44 (22 LOC)src/components/ErrorBoundary.tsx
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-teal-50">
<div className="text-center p-8 bg-white/80 backdrop-blur-md rounded-3xl shadow-xl max-w-sm">
<h1 className="text-2xl font-bold text-teal-800 mb-4">Something went wrong</h1>
<p className="text-gray-600 mb-6">An unexpected error occurred.</p>
<button
onClick={() => {
this.setState({ hasError: false });
window.location.reload();
}}
className="px-6 py-3 bg-teal-600 hover:bg-teal-700 text-white rounded-xl font-bold transition-all"
>
Reload
</button>
</div>
</div>
);
}
return this.props.children;
}Footer function · typescript · L5-L29 (25 LOC)src/components/Footer.tsx
export function Footer() {
const { lang, gameScreen } = useGameStore();
return (
<div className="w-full text-center pb-6 pt-4 px-4 opacity-90 flex flex-col items-center gap-2 z-10 transition-all duration-300">
<p className={`text-sm font-semibold uppercase tracking-widest ${gameScreen === 'playing' ? 'text-gray-700' : 'text-teal-800'}`}>
{t('support', lang)}
</p>
<a
href="https://www.instagram.com/yaseenyouthtours/"
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col items-center bg-white/60 hover:bg-white/95 backdrop-blur-sm px-8 py-3 rounded-3xl shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all border border-black/5"
>
<span className="flex items-center gap-2 font-extrabold text-gray-800 text-lg group-hover:text-teal-700 transition-colors">
<Instagram className="w-5 h-5 text-pink-600 drop-shadow-sm" />
Yaseen Youth
</span>
<span classNaCitation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
GameOverScreen function · typescript · L6-L54 (49 LOC)src/components/GameOverScreen.tsx
export function GameOverScreen() {
const { lang, players, playerScores, questionsPerPlayer, confirmReset } = useGameStore();
const ranked = players
.map((p, i) => ({ ...p, score: playerScores[i] }))
.sort((a, b) => b.score - a.score);
const topScore = ranked[0]?.score ?? 0;
const shareMessage = t('shareScoreMsg', lang)
.replace('{score}', String(topScore))
.replace('{total}', String(questionsPerPlayer));
return (
<div className="bg-white/90 backdrop-blur-lg p-10 rounded-3xl shadow-2xl w-full max-w-lg mx-auto text-center animate-fade-in">
<h2 className="text-5xl font-extrabold text-teal-800 mb-8">{t('gameOver', lang)}</h2>
<div className="space-y-4 mb-10">
{ranked.map((p, index) => (
<div
key={p.id}
className={`flex justify-between items-center p-5 rounded-2xl ${
index === 0 ? 'bg-yellow-100 border-2 border-yellow-300 scale-105 shadow-md' : 'bg-gray-50'
}`}
>
ModeSelectScreen function · typescript · L12-L84 (73 LOC)src/components/ModeSelectScreen.tsx
export function ModeSelectScreen({ onLocal, onOnline }: ModeSelectScreenProps) {
const { lang, toggleLang } = useGameStore();
const onlineCount = usePresence();
const isRTL = lang === 'ar';
return (
<div className="bg-white/80 backdrop-blur-md p-8 rounded-3xl shadow-xl w-full max-w-md mx-auto text-center relative">
<button
onClick={toggleLang}
className="absolute top-4 right-4 flex items-center gap-2 px-3 py-1.5 bg-white/50 hover:bg-white/80 rounded-full shadow-sm backdrop-blur-sm transition-all font-medium text-sm"
aria-label={t('switchLangLabel', lang)}
>
<Globe className="w-4 h-4" aria-hidden="true" />
{t('switchLangText', lang)}
</button>
<p
className="text-teal-700/60 font-medium mb-6 mt-2 tracking-widest text-lg"
style={{ fontFamily: '"Traditional Arabic", "Amiri", "Scheherazade New", serif' }}
>
{t('basmala', lang)}
</p>
<MoonIcon className="w-20 h-20 text-tealMoonIcon function · typescript · L1-L7 (7 LOC)src/components/MoonIcon.tsx
export function MoonIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
);
}ConnectionStatus function · typescript · L5-L18 (14 LOC)src/components/online/ConnectionStatus.tsx
export function ConnectionStatus() {
const { connected, screen } = useOnlineStore();
const { lang } = useGameStore();
// Only show when disconnected during an active session
if (connected || screen === 'setup') return null;
return (
<div className="flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-yellow-50 border border-yellow-200 text-yellow-700 text-sm font-medium mb-4 animate-pulse">
<div className="w-2 h-2 rounded-full bg-yellow-500" />
{t('reconnecting', lang)}
</div>
);
}LobbyScreen function · typescript · L17-L211 (195 LOC)src/components/online/LobbyScreen.tsx
export function LobbyScreen({ onBack }: LobbyScreenProps) {
const { lang, toggleLang } = useGameStore();
const {
roomCode, players, hostId, myPlayerId,
questionsPerRound, maxPlayers,
startGame, setQuestionCount, updateName,
} = useOnlineStore();
const [copied, setCopied] = useState(false);
const [editingName, setEditingName] = useState(false);
const [nameDraft, setNameDraft] = useState('');
const nameInputRef = useRef<HTMLInputElement>(null);
const isRTL = lang === 'ar';
const isHost = myPlayerId === hostId;
const connectedCount = players.filter(p => p.connected).length;
useEffect(() => {
if (editingName) nameInputRef.current?.focus();
}, [editingName]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(roomCode || '');
} catch {
const textarea = document.createElement('textarea');
textarea.value = roomCode || '';
document.body.appendChild(textarea);
textarea.select();
documOnlineGameOverScreen function · typescript · L12-L101 (90 LOC)src/components/online/OnlineGameOverScreen.tsx
export function OnlineGameOverScreen({ onBack }: OnlineGameOverScreenProps) {
const { lang, toggleLang } = useGameStore();
const { rankings, myPlayerId, hostId, questionsPerRound, restartGame, disconnect } = useOnlineStore();
const isHost = myPlayerId === hostId;
if (!rankings || rankings.length === 0) return null;
const myRanking = rankings.find(r => r.id === myPlayerId);
const myScore = myRanking?.score ?? 0;
const shareMessage = t('shareScoreMsg', lang)
.replace('{score}', String(myScore))
.replace('{total}', String(questionsPerRound));
const handleLeave = () => {
disconnect();
onBack();
};
return (
<div className="bg-white/90 backdrop-blur-lg p-10 rounded-3xl shadow-2xl w-full max-w-lg mx-auto text-center animate-fade-in">
<div className="flex justify-end mb-4">
<button
onClick={toggleLang}
className="flex items-center gap-2 px-3 py-1.5 bg-white/50 hover:bg-white/80 rounded-full shadow-sm backdrop-blur-sOnlinePlayingScreen function · typescript · L16-L231 (216 LOC)src/components/online/OnlinePlayingScreen.tsx
export function OnlinePlayingScreen() {
const { lang, toggleLang } = useGameStore();
const {
players, scores, currentQuestionIndex, questionIds,
currentTurnPlayerId, myPlayerId, hostId,
selectedAnswer, showExplanation, turnDeadline, turnTimeoutSeconds,
lastAnswerCorrect, timeoutPlayerId,
submitAnswer, nextQuestion,
} = useOnlineStore();
const isRTL = lang === 'ar';
const isMyTurn = myPlayerId === currentTurnPlayerId;
const isHost = myPlayerId === hostId;
const questionId = questionIds[currentQuestionIndex];
const question = questionsDB.find(q => q.id === questionId);
if (!question) return null;
const qData = question[lang];
const currentPlayer = players.find(p => p.id === currentTurnPlayerId);
const currentPlayerIndex = players.findIndex(p => p.id === currentTurnPlayerId);
const cardTheme = CARD_THEMES[currentPlayerIndex % CARD_THEMES.length];
const answeredCorrectly = lastAnswerCorrect === true;
const wasTimeout = timeoutPlayerId !OnlineSetupScreen function · typescript · L12-L165 (154 LOC)src/components/online/OnlineSetupScreen.tsx
export function OnlineSetupScreen({ onBack }: OnlineSetupScreenProps) {
const { lang, toggleLang } = useGameStore();
const { createRoom, joinRoom, error, clearError, connecting } = useOnlineStore();
const [name, setName] = useState(() => localStorage.getItem('kyd_player_name') || '');
const [lastRoom] = useState(() => getLastRoom());
const [roomCode, setRoomCode] = useState(() => {
const params = new URLSearchParams(window.location.search);
return params.get('room')?.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 4) || '';
});
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('room')) {
window.history.replaceState({}, '', window.location.pathname);
}
}, []);
const isRTL = lang === 'ar';
const handleCreate = () => {
if (connecting) return;
const playerName = name.trim() || (lang === 'ar' ? 'لاعب' : 'Player');
localStorage.setItem('kyd_player_name', playerName);
createRoom(playerNRepobility analyzer · published findings · https://repobility.com
TurnTimer function · typescript · L8-L43 (36 LOC)src/components/online/TurnTimer.tsx
export function TurnTimer({ deadline, totalSeconds }: TurnTimerProps) {
const [remaining, setRemaining] = useState(totalSeconds);
useEffect(() => {
const update = () => {
const ms = deadline - Date.now();
setRemaining(Math.max(0, Math.ceil(ms / 1000)));
};
update();
const interval = setInterval(update, 200);
return () => clearInterval(interval);
}, [deadline]);
const fraction = remaining / totalSeconds;
const isUrgent = remaining <= 5;
const isWarning = remaining <= 15 && !isUrgent;
const barColor = isUrgent ? 'bg-red-500' : isWarning ? 'bg-yellow-500' : 'bg-teal-500';
const textColor = isUrgent ? 'text-red-600' : isWarning ? 'text-yellow-600' : 'text-teal-600';
return (
<div className="w-full">
<div className="flex justify-end mb-1">
<span className={`text-sm font-bold tabular-nums ${textColor} ${isUrgent ? 'animate-pulse' : ''}`}>
{remaining}s
</span>
</div>
<div className="w-full h-PlayingScreen function · typescript · L12-L147 (136 LOC)src/components/PlayingScreen.tsx
export function PlayingScreen() {
const {
lang, currentTurn, players, playerScores,
questionIndices, questionsPerPlayer, selectedAnswer,
showExplanation, handleAnswerSelect, nextTurn, activeQuestion: getActiveQuestion,
} = useGameStore();
const activeQuestion = getActiveQuestion();
if (!activeQuestion) return null;
const isRTL = lang === 'ar';
const cardTheme = CARD_THEMES[currentTurn];
const qData = activeQuestion[lang];
const isCorrect = selectedAnswer === qData.ans;
return (
<div className="w-full max-w-2xl mx-auto flex flex-col h-full animate-fade-in">
<div className="flex justify-between items-end mb-6">
<div className={`px-6 py-3 rounded-2xl shadow-sm border-2 ${cardTheme} flex items-center gap-3 backdrop-blur-md`}>
<div className="w-10 h-10 rounded-full bg-white flex items-center justify-center font-bold text-xl shadow-sm">
{currentTurn + 1}
</div>
<div>
<p className="text-xsResetModal function · typescript · L6-L88 (83 LOC)src/components/ResetModal.tsx
export function ResetModal() {
const { lang, showResetModal, setShowResetModal, confirmReset } = useGameStore();
const dialogRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!showResetModal) return;
previousFocusRef.current = document.activeElement as HTMLElement;
// Focus the dialog
const timer = setTimeout(() => {
dialogRef.current?.focus();
}, 50);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setShowResetModal(false);
return;
}
// Focus trap
if (e.key === 'Tab' && dialogRef.current) {
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
ScoreCardContent function · typescript · L31-L91 (61 LOC)src/components/ScoreCard.tsx
function ScoreCardContent({ rankings, totalQuestions, lang }: Omit<ShareScoreCardProps, 'shareMessage'>) {
const isRTL = lang === 'ar';
return (
<div
style={{
width: 400,
padding: 32,
background: 'linear-gradient(135deg, #0d9488 0%, #0891b2 100%)',
borderRadius: 24,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
direction: isRTL ? 'rtl' : 'ltr',
}}
>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<svg width="48" height="48" viewBox="0 0 24 24" fill="white" style={{ margin: '0 auto 8px', display: 'block' }}>
<path d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
<h2 style={{ color: 'white', fontSize: 24, fontWeight: 800, margin: 0 }}>
{t('title', lang)}
</h2>
</div>
ShareScoreCard function · typescript · L93-L167 (75 LOC)src/components/ScoreCard.tsx
export function ShareScoreCard({ rankings, totalQuestions, lang, shareMessage }: ShareScoreCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const [copied, setCopied] = useState(false);
const [busy, setBusy] = useState(false);
const handleCopyImage = async () => {
if (!cardRef.current || busy) return;
setBusy(true);
try {
const dataUrl = await toPng(cardRef.current, { quality: 0.95, pixelRatio: 2 });
const res = await fetch(dataUrl);
const blob = await res.blob();
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob }),
]);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback: download if clipboard write fails
try {
const dataUrl = await toPng(cardRef.current, { quality: 0.95, pixelRatio: 2 });
const a = document.createElement('a');
a.href = dataUrl;
a.download = 'knowyourdeen-score.png';
document.body.appSetupScreen function · typescript · L12-L107 (96 LOC)src/components/SetupScreen.tsx
export function SetupScreen({ onBack }: SetupScreenProps) {
const { lang, toggleLang, players, handlePlayerCountChange, updatePlayerName, startGame } = useGameStore();
const isRTL = lang === 'ar';
return (
<div className="bg-white/80 backdrop-blur-md p-8 rounded-3xl shadow-xl w-full max-w-md mx-auto text-center">
<div className="flex items-center justify-between mb-6">
<button
onClick={onBack}
className="flex items-center gap-2 text-teal-600 hover:text-teal-800 font-medium transition-colors"
>
<ArrowLeft className={`w-5 h-5 ${isRTL ? 'rotate-180' : ''}`} />
{t('back', lang)}
</button>
<button
onClick={toggleLang}
className="flex items-center gap-2 px-3 py-1.5 bg-white/50 hover:bg-white/80 rounded-full shadow-sm backdrop-blur-sm transition-all font-medium text-sm"
aria-label={t('switchLangLabel', lang)}
>
<Globe className="w-4 h-4" aria-hidden="true" /SharePopover function · typescript · L12-L88 (77 LOC)src/components/SharePopover.tsx
export function SharePopover() {
const { lang } = useGameStore();
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const popoverRef = useRef<HTMLDivElement>(null);
const message = t('shareAppMsg', lang);
useEffect(() => {
if (!open) return;
const handleClickOutside = (e: MouseEvent) => {
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(message);
} catch {
const textarea = document.createElement('textarea');
textarea.value = message;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
setCopied(truTopBar function · typescript · L9-L47 (39 LOC)src/components/TopBar.tsx
export function TopBar() {
const { lang, gameScreen, currentTurn, toggleLang, setGameScreen, setShowResetModal } = useGameStore();
return (
<div className="w-full max-w-4xl p-4 flex justify-between items-center z-10">
<div className="flex items-center gap-2">
<MoonIcon className={`w-8 h-8 ${gameScreen === 'playing' ? MOON_COLORS[currentTurn] : 'text-teal-600'}`} />
<h1 className="text-xl font-bold opacity-80">{t('title', lang)}</h1>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setGameScreen(gameScreen === 'library' ? 'setup' : 'library')}
className="p-2 bg-white/50 hover:bg-white/80 rounded-full shadow-sm backdrop-blur-sm transition-all text-teal-600"
title={t('libraryLabel', lang)}
aria-label={t('libraryLabel', lang)}
>
<BookOpen className="w-5 h-5" aria-hidden="true" />
</button>
<SharePopover />
<button
onClick={() Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
shuffleArray function · typescript · L42-L49 (8 LOC)src/hooks/useGameStore.ts
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}generateRoomCode function · typescript · L7-L14 (8 LOC)src/hooks/useOnlineStore.ts
function generateRoomCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let code = '';
for (let i = 0; i < 4; i++) {
code += chars[Math.floor(Math.random() * chars.length)];
}
return code;
}getOrCreatePlayerId function · typescript · L18-L30 (13 LOC)src/hooks/useOnlineStore.ts
function getOrCreatePlayerId(roomCode: string): string {
const key = PLAYER_ID_PREFIX + roomCode;
const stored = localStorage.getItem(key);
if (stored) {
try {
const { id, ts } = JSON.parse(stored);
if (Date.now() - ts < 60 * 60 * 1000) return id;
} catch { /* ignore corrupt entries */ }
}
const id = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
localStorage.setItem(key, JSON.stringify({ id, ts: Date.now() }));
return id;
}page 1 / 2next ›