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