Fix: reload login state from superbase

This commit is contained in:
Zakaria 2026-05-14 17:31:00 -04:00
parent fe5649dc2d
commit 677cae0125
5 changed files with 160 additions and 59 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -114,8 +114,7 @@ a {
.brand small, .brand small,
.deck, .deck,
.status-row, .status-row,
.alert, .alert {
.definition-alert {
color: var(--muted); color: var(--muted);
} }
@ -360,29 +359,6 @@ h2 {
transform: translateY(-6px); transform: translateY(-6px);
} }
.definition-alert {
pointer-events: auto;
width: min(440px, calc(100vw - 32px));
text-align: start;
}
.definition-alert strong {
display: block;
margin-bottom: 6px;
color: var(--fg);
font-family: var(--font-display);
font-size: 1.2rem;
font-weight: 500;
}
.definition-alert::after {
display: block;
margin-top: 10px;
color: var(--muted);
content: "Click to dismiss";
font-size: 0.8rem;
}
.stats-modal { .stats-modal {
width: min(520px, calc(100vw - 32px)); width: min(520px, calc(100vw - 32px));
padding: 0; padding: 0;
@ -462,6 +438,59 @@ h2 {
line-height: 1.5; line-height: 1.5;
} }
.leaderboard-result {
display: grid;
gap: 12px;
padding: 14px;
border: 1px solid color-mix(in oklch, var(--accent), var(--border) 45%);
border-radius: 16px;
background: color-mix(in oklch, var(--accent), var(--surface) 88%);
color: var(--fg);
line-height: 1.55;
}
.leaderboard-result[hidden] {
display: none;
}
.leaderboard-result-summary {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid color-mix(in oklch, var(--accent), var(--border) 62%);
padding-bottom: 10px;
}
.leaderboard-result strong {
display: block;
font-family: var(--font-display);
font-size: 1.35rem;
font-weight: 500;
}
.leaderboard-result span {
color: var(--muted);
}
.leaderboard-result-summary span {
flex: 0 0 auto;
font-size: 0.9rem;
font-weight: 900;
}
.leaderboard-definition {
display: grid;
gap: 6px;
}
.leaderboard-result em {
display: block;
margin-top: 10px;
color: var(--muted);
font-style: normal;
}
.definition-panel { .definition-panel {
display: grid; display: grid;
gap: 10px; gap: 10px;

View File

@ -97,6 +97,7 @@
<button type="button" class="leaderboard-tab" data-leaderboard-scope="today">Today</button> <button type="button" class="leaderboard-tab" data-leaderboard-scope="today">Today</button>
<button type="button" class="leaderboard-tab" data-leaderboard-scope="all">All Time</button> <button type="button" class="leaderboard-tab" data-leaderboard-scope="all">All Time</button>
</div> </div>
<div class="leaderboard-result" id="leaderboard-result" hidden></div>
<p class="leaderboard-countdown" id="leaderboard-countdown">Next word unlocks soon.</p> <p class="leaderboard-countdown" id="leaderboard-countdown">Next word unlocks soon.</p>
<div class="leaderboard-list" id="leaderboard-list" aria-live="polite"></div> <div class="leaderboard-list" id="leaderboard-list" aria-live="polite"></div>
</form> </form>

View File

@ -97,6 +97,7 @@
<button type="button" class="leaderboard-tab" data-leaderboard-scope="today">Today</button> <button type="button" class="leaderboard-tab" data-leaderboard-scope="today">Today</button>
<button type="button" class="leaderboard-tab" data-leaderboard-scope="all">All Time</button> <button type="button" class="leaderboard-tab" data-leaderboard-scope="all">All Time</button>
</div> </div>
<div class="leaderboard-result" id="leaderboard-result" hidden></div>
<p class="leaderboard-countdown" id="leaderboard-countdown">Next word unlocks soon.</p> <p class="leaderboard-countdown" id="leaderboard-countdown">Next word unlocks soon.</p>
<div class="leaderboard-list" id="leaderboard-list" aria-live="polite"></div> <div class="leaderboard-list" id="leaderboard-list" aria-live="polite"></div>
</form> </form>

134
script.js
View File

@ -25,6 +25,7 @@ const DANCE_ANIMATION_DURATION = 500
const STATS_KEY = "fancy-wordle-stats-v2" const STATS_KEY = "fancy-wordle-stats-v2"
const LOCAL_ROUND_KEY = "fancy-wordle-hourly-round-v1" const LOCAL_ROUND_KEY = "fancy-wordle-hourly-round-v1"
const PLAY_INTERVAL_MS = 60 * 60 * 1000 const PLAY_INTERVAL_MS = 60 * 60 * 1000
const GUESS_TIMEOUT_MS = 10000
const KEY_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"] const KEY_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
const FALLBACK_TARGET_WORDS = [ const FALLBACK_TARGET_WORDS = [
"about", "about",
@ -74,6 +75,7 @@ const statsButton = document.getElementById("Stats-button")
const leaderboardButton = document.getElementById("leaderboard-button") const leaderboardButton = document.getElementById("leaderboard-button")
const leaderboardModal = document.getElementById("leaderboard-modal") const leaderboardModal = document.getElementById("leaderboard-modal")
const leaderboardList = document.getElementById("leaderboard-list") const leaderboardList = document.getElementById("leaderboard-list")
const leaderboardResult = document.getElementById("leaderboard-result")
const leaderboardCountdown = document.getElementById("leaderboard-countdown") const leaderboardCountdown = document.getElementById("leaderboard-countdown")
const leaderboardTabs = document.querySelectorAll("[data-leaderboard-scope]") const leaderboardTabs = document.querySelectorAll("[data-leaderboard-scope]")
const authButton = document.getElementById("auth-button") const authButton = document.getElementById("auth-button")
@ -210,9 +212,9 @@ async function initializeAuth() {
authProfile = session ? await getAuthProfile() : null authProfile = session ? await getAuthProfile() : null
updateAuthControls() updateAuthControls()
client.auth.onAuthStateChange(async (event, session) => { client.auth.onAuthStateChange((event, session) => {
authSession = session authSession = session
authProfile = session ? await getAuthProfile() : null authProfile = null
updateAuthControls() updateAuthControls()
if (event === "PASSWORD_RECOVERY") { if (event === "PASSWORD_RECOVERY") {
@ -221,7 +223,12 @@ async function initializeAuth() {
return return
} }
if (event === "SIGNED_IN") window.location.reload() if (session) {
getAuthProfile().then(profile => {
authProfile = profile
updateAuthControls()
})
}
}) })
} catch (error) { } catch (error) {
console.warn("Supabase auth unavailable:", error) console.warn("Supabase auth unavailable:", error)
@ -410,6 +417,7 @@ function applyHourlyRound(round) {
hourlyRound = round hourlyRound = round
targetWord = round.word || "" targetWord = round.word || ""
if (targetWord) dictionarySet.add(targetWord) if (targetWord) dictionarySet.add(targetWord)
resetBoardForRound()
restoreGuesses(round.guesses || []) restoreGuesses(round.guesses || [])
currentGuessIndex = round.completedAt ? round.guessCount || currentGuessIndex : currentGuessIndex currentGuessIndex = round.completedAt ? round.guessCount || currentGuessIndex : currentGuessIndex
updateTriesStatus() updateTriesStatus()
@ -429,6 +437,22 @@ function applyHourlyRound(round) {
startInteraction() startInteraction()
} }
function resetBoardForRound() {
[...guessGrid.children].forEach(tile => {
tile.textContent = ""
tile.removeAttribute("data-state")
tile.removeAttribute("data-letter")
tile.setAttribute("aria-label", "Empty letter")
})
keyboard.querySelectorAll(".wrong, .wrong-position, .correct").forEach(key => {
key.classList.remove("wrong", "wrong-position", "correct")
})
currentGuessIndex = 0
currentTileIndex = 0
}
function restoreGuesses(guesses) { function restoreGuesses(guesses) {
if (!Array.isArray(guesses) || guesses.length === 0) { if (!Array.isArray(guesses) || guesses.length === 0) {
currentGuessIndex = 0 currentGuessIndex = 0
@ -605,7 +629,10 @@ async function signInWithPassword(event) {
if (error) { if (error) {
setCreateAccountMode(true) setCreateAccountMode(true)
authStatus.textContent = `${readableAuthError(error)} Create an account below if this is your first time.` authStatus.textContent = `${readableAuthError(error)} Create an account below if this is your first time.`
return
} }
window.location.reload()
} }
async function isUsernameAvailable(username) { async function isUsernameAvailable(username) {
@ -809,7 +836,7 @@ async function submitGuess() {
const states = normalizeGuessStates(result.states) const states = normalizeGuessStates(result.states)
if (states.length !== WORD_LENGTH) { if (states.length !== WORD_LENGTH) {
recoverFromGuessError(activeTiles, "Guess could not be scored") await recoverFromGuessError(activeTiles, "Guess could not be scored")
return return
} }
@ -827,7 +854,7 @@ async function resolveGuess(guess, activeTiles) {
return await submitRemoteGuess(guess) return await submitRemoteGuess(guess)
} catch (error) { } catch (error) {
console.warn("Failed to submit guess:", error) console.warn("Failed to submit guess:", error)
recoverFromGuessError(activeTiles, error.message || "Guess could not be saved") await recoverFromGuessError(activeTiles, error.message || "Guess could not be saved")
return null return null
} }
} }
@ -846,10 +873,22 @@ async function submitRemoteGuess(guess) {
const client = getSupabaseClient() const client = getSupabaseClient()
if (!client || !hourlyRound?.id) throw new Error("Sign in again to save this guess") if (!client || !hourlyRound?.id) throw new Error("Sign in again to save this guess")
const { data, error } = await client.rpc("submit_guess", { const { data: { session }, error: sessionError } = await withTimeout(
client.auth.getSession(),
GUESS_TIMEOUT_MS,
"Session check timed out. Try again."
)
if (sessionError || !session) throw new Error("Please sign in again.")
const { data, error } = await withTimeout(
client.rpc("submit_guess", {
target_round_id: hourlyRound.id, target_round_id: hourlyRound.id,
submitted_guess: guess submitted_guess: guess
}) }),
GUESS_TIMEOUT_MS,
"Connection timed out. Resyncing your round."
)
if (error) throw error if (error) throw error
@ -867,6 +906,15 @@ async function submitRemoteGuess(guess) {
} }
} }
function withTimeout(promise, timeout, message) {
let timeoutId
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(message)), timeout)
})
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId))
}
function normalizeGuessStates(states) { function normalizeGuessStates(states) {
if (Array.isArray(states)) return states if (Array.isArray(states)) return states
@ -882,12 +930,22 @@ function normalizeGuessStates(states) {
return [] return []
} }
function recoverFromGuessError(activeTiles, message) { async function recoverFromGuessError(activeTiles, message) {
showAlert(message) showAlert(message)
shakeTiles(activeTiles) shakeTiles(activeTiles)
roundStatus.textContent = "Try again" roundStatus.textContent = "Try again"
isAnimating = false isAnimating = false
startInteraction() if (hourlyRound?.backend === "supabase") await resyncRemoteRound()
if (!gameFinished) startInteraction()
}
async function resyncRemoteRound() {
try {
const round = await startRemoteHourlyRound()
if (round) applyHourlyRound(round)
} catch (error) {
console.warn("Failed to resync round:", error)
}
} }
function isValidGuess(guess) { function isValidGuess(guess) {
@ -1018,7 +1076,7 @@ function checkWinLose(guess, tiles, roundResult) {
danceTiles(tiles) danceTiles(tiles)
celebrateWithConfetti() celebrateWithConfetti()
roundStatus.textContent = `Solved in ${currentGuessIndex}` roundStatus.textContent = `Solved in ${currentGuessIndex}`
setTimeout(() => showWordDefinition(targetWord, true), 1800) loadWordDefinition(targetWord, true)
startLockCountdown(2400) startLockCountdown(2400)
showLeaderboardAfterCompletion() showLeaderboardAfterCompletion()
stopInteraction() stopInteraction()
@ -1031,9 +1089,8 @@ function checkWinLose(guess, tiles, roundResult) {
gameFinished = true gameFinished = true
lastResult = { won: false, guesses: MAX_GUESSES, word: targetWord } lastResult = { won: false, guesses: MAX_GUESSES, word: targetWord }
saveGameResult(lastResult) saveGameResult(lastResult)
showAlert(targetWord.toUpperCase(), null)
roundStatus.textContent = "Round complete" roundStatus.textContent = "Round complete"
showWordDefinition(targetWord, false) loadWordDefinition(targetWord, false)
startLockCountdown(2400) startLockCountdown(2400)
showLeaderboardAfterCompletion() showLeaderboardAfterCompletion()
stopInteraction() stopInteraction()
@ -1051,6 +1108,7 @@ function updateTriesStatus() {
function showLeaderboardAfterCompletion() { function showLeaderboardAfterCompletion() {
setTimeout(() => { setTimeout(() => {
if (statsModal.open || authModal?.open) return if (statsModal.open || authModal?.open) return
if (leaderboardModal?.open) return
leaderboardScope = "hour" leaderboardScope = "hour"
leaderboardTabs.forEach(tab => { leaderboardTabs.forEach(tab => {
tab.classList.toggle("active", tab.dataset.leaderboardScope === leaderboardScope) tab.classList.toggle("active", tab.dataset.leaderboardScope === leaderboardScope)
@ -1124,11 +1182,6 @@ function playCelebrationSound() {
}) })
} }
async function showWordDefinition(word, isWin = true) {
const definition = await loadWordDefinition(word, isWin)
showDefinitionAlert(definition)
}
async function loadWordDefinition(word, isWin = true) { async function loadWordDefinition(word, isWin = true) {
try { try {
const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`) const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`)
@ -1158,24 +1211,10 @@ async function loadWordDefinition(word, isWin = true) {
} }
renderStatsDefinition() renderStatsDefinition()
renderLeaderboardResult()
return lastDefinition return lastDefinition
} }
function showDefinitionAlert({ title, body, example, isWin }) {
const alert = document.createElement("button")
alert.type = "button"
alert.className = `alert definition-alert ${isWin ? "win-definition" : "lose-definition"}`
alert.innerHTML = `
<strong>${escapeHtml(title)}</strong>
<span>${escapeHtml(body)}</span>
${example ? `<span><br><br>Example: “${escapeHtml(example)}”</span>` : ""}
`
alertContainer.prepend(alert)
alert.addEventListener("click", () => dismissAlert(alert))
setTimeout(() => dismissAlert(alert), 10000)
}
function escapeHtml(value) { function escapeHtml(value) {
return String(value) return String(value)
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
@ -1228,6 +1267,7 @@ async function openStats() {
async function openLeaderboard() { async function openLeaderboard() {
renderLeaderboardLoading() renderLeaderboardLoading()
renderLeaderboardResult()
leaderboardModal?.showModal() leaderboardModal?.showModal()
await renderLeaderboard() await renderLeaderboard()
} }
@ -1408,6 +1448,36 @@ function renderLeaderboardLoading() {
leaderboardList.innerHTML = '<div class="leaderboard-empty">Loading this hour\'s finishers...</div>' leaderboardList.innerHTML = '<div class="leaderboard-empty">Loading this hour\'s finishers...</div>'
updateLeaderboardCountdown() updateLeaderboardCountdown()
renderLeaderboardResult()
}
function renderLeaderboardResult() {
if (!leaderboardResult) return
if (!lastResult) {
leaderboardResult.hidden = true
leaderboardResult.innerHTML = ""
return
}
leaderboardResult.hidden = false
const status = lastResult.won ? `Solved in ${lastResult.guesses}/6` : "Round complete"
const definition = lastDefinition
? `
<span>${escapeHtml(lastDefinition.body)}</span>
${lastDefinition.example ? `<em>Example: ${escapeHtml(lastDefinition.example)}</em>` : ""}
`
: "<span>Loading definition...</span>"
leaderboardResult.innerHTML = `
<div class="leaderboard-result-summary">
<strong>${escapeHtml(lastResult.word || "Word reveal")}</strong>
<span>${status}</span>
</div>
<div class="leaderboard-definition">
${definition}
</div>
`
} }
async function renderLeaderboard() { async function renderLeaderboard() {