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,
.deck,
.status-row,
.alert,
.definition-alert {
.alert {
color: var(--muted);
}
@ -360,29 +359,6 @@ h2 {
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 {
width: min(520px, calc(100vw - 32px));
padding: 0;
@ -462,6 +438,59 @@ h2 {
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 {
display: grid;
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="all">All Time</button>
</div>
<div class="leaderboard-result" id="leaderboard-result" hidden></div>
<p class="leaderboard-countdown" id="leaderboard-countdown">Next word unlocks soon.</p>
<div class="leaderboard-list" id="leaderboard-list" aria-live="polite"></div>
</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="all">All Time</button>
</div>
<div class="leaderboard-result" id="leaderboard-result" hidden></div>
<p class="leaderboard-countdown" id="leaderboard-countdown">Next word unlocks soon.</p>
<div class="leaderboard-list" id="leaderboard-list" aria-live="polite"></div>
</form>

138
script.js
View File

@ -25,6 +25,7 @@ const DANCE_ANIMATION_DURATION = 500
const STATS_KEY = "fancy-wordle-stats-v2"
const LOCAL_ROUND_KEY = "fancy-wordle-hourly-round-v1"
const PLAY_INTERVAL_MS = 60 * 60 * 1000
const GUESS_TIMEOUT_MS = 10000
const KEY_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
const FALLBACK_TARGET_WORDS = [
"about",
@ -74,6 +75,7 @@ const statsButton = document.getElementById("Stats-button")
const leaderboardButton = document.getElementById("leaderboard-button")
const leaderboardModal = document.getElementById("leaderboard-modal")
const leaderboardList = document.getElementById("leaderboard-list")
const leaderboardResult = document.getElementById("leaderboard-result")
const leaderboardCountdown = document.getElementById("leaderboard-countdown")
const leaderboardTabs = document.querySelectorAll("[data-leaderboard-scope]")
const authButton = document.getElementById("auth-button")
@ -210,9 +212,9 @@ async function initializeAuth() {
authProfile = session ? await getAuthProfile() : null
updateAuthControls()
client.auth.onAuthStateChange(async (event, session) => {
client.auth.onAuthStateChange((event, session) => {
authSession = session
authProfile = session ? await getAuthProfile() : null
authProfile = null
updateAuthControls()
if (event === "PASSWORD_RECOVERY") {
@ -221,7 +223,12 @@ async function initializeAuth() {
return
}
if (event === "SIGNED_IN") window.location.reload()
if (session) {
getAuthProfile().then(profile => {
authProfile = profile
updateAuthControls()
})
}
})
} catch (error) {
console.warn("Supabase auth unavailable:", error)
@ -410,6 +417,7 @@ function applyHourlyRound(round) {
hourlyRound = round
targetWord = round.word || ""
if (targetWord) dictionarySet.add(targetWord)
resetBoardForRound()
restoreGuesses(round.guesses || [])
currentGuessIndex = round.completedAt ? round.guessCount || currentGuessIndex : currentGuessIndex
updateTriesStatus()
@ -429,6 +437,22 @@ function applyHourlyRound(round) {
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) {
if (!Array.isArray(guesses) || guesses.length === 0) {
currentGuessIndex = 0
@ -605,7 +629,10 @@ async function signInWithPassword(event) {
if (error) {
setCreateAccountMode(true)
authStatus.textContent = `${readableAuthError(error)} Create an account below if this is your first time.`
return
}
window.location.reload()
}
async function isUsernameAvailable(username) {
@ -809,7 +836,7 @@ async function submitGuess() {
const states = normalizeGuessStates(result.states)
if (states.length !== WORD_LENGTH) {
recoverFromGuessError(activeTiles, "Guess could not be scored")
await recoverFromGuessError(activeTiles, "Guess could not be scored")
return
}
@ -827,7 +854,7 @@ async function resolveGuess(guess, activeTiles) {
return await submitRemoteGuess(guess)
} catch (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
}
}
@ -846,10 +873,22 @@ async function submitRemoteGuess(guess) {
const client = getSupabaseClient()
if (!client || !hourlyRound?.id) throw new Error("Sign in again to save this guess")
const { data, error } = await client.rpc("submit_guess", {
target_round_id: hourlyRound.id,
submitted_guess: 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,
submitted_guess: guess
}),
GUESS_TIMEOUT_MS,
"Connection timed out. Resyncing your round."
)
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) {
if (Array.isArray(states)) return states
@ -882,12 +930,22 @@ function normalizeGuessStates(states) {
return []
}
function recoverFromGuessError(activeTiles, message) {
async function recoverFromGuessError(activeTiles, message) {
showAlert(message)
shakeTiles(activeTiles)
roundStatus.textContent = "Try again"
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) {
@ -1018,7 +1076,7 @@ function checkWinLose(guess, tiles, roundResult) {
danceTiles(tiles)
celebrateWithConfetti()
roundStatus.textContent = `Solved in ${currentGuessIndex}`
setTimeout(() => showWordDefinition(targetWord, true), 1800)
loadWordDefinition(targetWord, true)
startLockCountdown(2400)
showLeaderboardAfterCompletion()
stopInteraction()
@ -1031,9 +1089,8 @@ function checkWinLose(guess, tiles, roundResult) {
gameFinished = true
lastResult = { won: false, guesses: MAX_GUESSES, word: targetWord }
saveGameResult(lastResult)
showAlert(targetWord.toUpperCase(), null)
roundStatus.textContent = "Round complete"
showWordDefinition(targetWord, false)
loadWordDefinition(targetWord, false)
startLockCountdown(2400)
showLeaderboardAfterCompletion()
stopInteraction()
@ -1051,6 +1108,7 @@ function updateTriesStatus() {
function showLeaderboardAfterCompletion() {
setTimeout(() => {
if (statsModal.open || authModal?.open) return
if (leaderboardModal?.open) return
leaderboardScope = "hour"
leaderboardTabs.forEach(tab => {
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) {
try {
const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`)
@ -1158,24 +1211,10 @@ async function loadWordDefinition(word, isWin = true) {
}
renderStatsDefinition()
renderLeaderboardResult()
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) {
return String(value)
.replaceAll("&", "&amp;")
@ -1228,6 +1267,7 @@ async function openStats() {
async function openLeaderboard() {
renderLeaderboardLoading()
renderLeaderboardResult()
leaderboardModal?.showModal()
await renderLeaderboard()
}
@ -1408,6 +1448,36 @@ function renderLeaderboardLoading() {
leaderboardList.innerHTML = '<div class="leaderboard-empty">Loading this hour\'s finishers...</div>'
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() {