Fix: reload login state from superbase
This commit is contained in:
parent
fe5649dc2d
commit
677cae0125
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
138
script.js
@ -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("&", "&")
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user