Update Version (Beta): login + leaderboard/ word definition

This commit is contained in:
Zakaria 2026-05-13 11:24:27 -04:00
parent 66f8207985
commit fe5649dc2d
6 changed files with 60 additions and 107 deletions

View File

@ -462,43 +462,52 @@ h2 {
line-height: 1.5; line-height: 1.5;
} }
.history-panel { .definition-panel {
display: grid; display: grid;
gap: 10px; gap: 10px;
margin-bottom: 22px; margin-bottom: 22px;
} }
.history-panel h3 { .definition-panel h3 {
margin: 0; margin: 0;
font-family: var(--font-display); font-family: var(--font-display);
font-size: 1.15rem; font-size: 1.15rem;
font-weight: 500; font-weight: 500;
} }
.history-list { .stats-definition {
display: grid; padding: 14px;
gap: 8px;
}
.history-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
background: var(--bg); background: var(--bg);
color: var(--muted);
line-height: 1.55;
text-align: start;
} }
.history-word { .stats-definition strong {
display: block;
margin-bottom: 6px;
color: var(--fg);
font-family: var(--font-display);
font-size: 1.35rem;
font-weight: 500;
line-height: 1.1;
}
.stats-definition .word-label {
font-family: var(--font-body);
font-size: 0.76rem;
font-weight: 900; font-weight: 900;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
} }
.history-meta { .stats-definition em {
display: block;
margin-top: 10px;
color: var(--muted); color: var(--muted);
font-size: 0.84rem; font-style: normal;
} }
.leaderboard-card { .leaderboard-card {

View File

@ -19,7 +19,7 @@ The database chooses the word from `public.wordle_words` based on the current UT
The hourly leaderboard uses completed authenticated rounds for the current UTC hour. It ranks wins first, then fewer guesses, then earliest completion time. The hourly leaderboard uses completed authenticated rounds for the current UTC hour. It ranks wins first, then fewer guesses, then earliest completion time.
Leaderboard tabs include this hour, today, and all time. Signed-in users are included even if their row falls outside the top 25. Stats also show recent personal history and this hour's average score summary. Leaderboard tabs include this hour, today, and all time. Signed-in users are included even if their row falls outside the top 25. Stats also show the current revealed word definition and this hour's average score summary.
`supabase/seed-word-data.sql` is generated from `targetWords.json` and `dictionary.json`: `supabase/seed-word-data.sql` is generated from `targetWords.json` and `dictionary.json`:

View File

@ -72,9 +72,9 @@
<div class="stats-grid" id="stats-grid"></div> <div class="stats-grid" id="stats-grid"></div>
<div class="stats-note" id="stats-note"></div> <div class="stats-note" id="stats-note"></div>
<div class="guess-bars" id="guess-bars" aria-label="Guess distribution"></div> <div class="guess-bars" id="guess-bars" aria-label="Guess distribution"></div>
<div class="history-panel"> <div class="definition-panel" id="stats-definition-panel">
<h3>Recent history</h3> <h3>Word reveal</h3>
<div class="history-list" id="history-list"></div> <div class="stats-definition" id="stats-definition"></div>
</div> </div>
<menu class="modal-actions"> <menu class="modal-actions">
<button type="button" class="button secondary" id="reset-stats">Reset stats</button> <button type="button" class="button secondary" id="reset-stats">Reset stats</button>

View File

@ -72,9 +72,9 @@
<div class="stats-grid" id="stats-grid"></div> <div class="stats-grid" id="stats-grid"></div>
<div class="stats-note" id="stats-note"></div> <div class="stats-note" id="stats-note"></div>
<div class="guess-bars" id="guess-bars" aria-label="Guess distribution"></div> <div class="guess-bars" id="guess-bars" aria-label="Guess distribution"></div>
<div class="history-panel"> <div class="definition-panel" id="stats-definition-panel">
<h3>Recent history</h3> <h3>Word reveal</h3>
<div class="history-list" id="history-list"></div> <div class="stats-definition" id="stats-definition"></div>
</div> </div>
<menu class="modal-actions"> <menu class="modal-actions">
<button type="button" class="button secondary" id="reset-stats">Reset stats</button> <button type="button" class="button secondary" id="reset-stats">Reset stats</button>

View File

@ -16,6 +16,7 @@ let authSession = null
let authProfile = null let authProfile = null
let leaderboardScope = "hour" let leaderboardScope = "hour"
let latestLeaderboardRows = [] let latestLeaderboardRows = []
let lastDefinition = null
const WORD_LENGTH = 5 const WORD_LENGTH = 5
const MAX_GUESSES = 6 const MAX_GUESSES = 6
@ -96,7 +97,7 @@ const statsModal = document.getElementById("stats-modal")
const statsGrid = document.getElementById("stats-grid") const statsGrid = document.getElementById("stats-grid")
const statsNote = document.getElementById("stats-note") const statsNote = document.getElementById("stats-note")
const guessBars = document.getElementById("guess-bars") const guessBars = document.getElementById("guess-bars")
const historyList = document.getElementById("history-list") const statsDefinition = document.getElementById("stats-definition")
const resetStatsButton = document.getElementById("reset-stats") const resetStatsButton = document.getElementById("reset-stats")
const shareResultsButton = document.getElementById("share-results") const shareResultsButton = document.getElementById("share-results")
@ -419,6 +420,7 @@ function applyHourlyRound(round) {
guesses: round.guessCount || MAX_GUESSES, guesses: round.guessCount || MAX_GUESSES,
word: round.word || "" word: round.word || ""
} }
if (round.word) loadWordDefinition(round.word, Boolean(round.won))
lockUntilNextWord() lockUntilNextWord()
return return
} }
@ -1123,6 +1125,11 @@ function playCelebrationSound() {
} }
async function showWordDefinition(word, isWin = true) { async function showWordDefinition(word, isWin = true) {
const definition = await loadWordDefinition(word, isWin)
showDefinitionAlert(definition)
}
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}`)
if (!response.ok) throw new Error(`API request failed: ${response.status}`) if (!response.ok) throw new Error(`API request failed: ${response.status}`)
@ -1135,20 +1142,23 @@ async function showWordDefinition(word, isWin = true) {
if (!definition) throw new Error("No definition found") if (!definition) throw new Error("No definition found")
showDefinitionAlert({ lastDefinition = {
title: `${word.toUpperCase()} ${partOfSpeech ? `(${partOfSpeech})` : ""}`, title: `${word.toUpperCase()} ${partOfSpeech ? `(${partOfSpeech})` : ""}`,
body: definition, body: definition,
example, example,
isWin isWin
}) }
} catch (error) { } catch (error) {
console.info("Definition lookup unavailable:", error) console.info("Definition lookup unavailable:", error)
showDefinitionAlert({ lastDefinition = {
title: word.toUpperCase(), title: word.toUpperCase(),
body: "Definition not available at the moment.", body: "Definition not available at the moment.",
isWin isWin
}) }
} }
renderStatsDefinition()
return lastDefinition
} }
function showDefinitionAlert({ title, body, example, isWin }) { function showDefinitionAlert({ title, body, example, isWin }) {
@ -1308,7 +1318,7 @@ async function renderStats() {
.join("") .join("")
renderStatsNote(summary) renderStatsNote(summary)
await renderHistory() renderStatsDefinition()
} }
function renderStatsNote(summary) { function renderStatsNote(summary) {
@ -1372,60 +1382,25 @@ async function getHourlySummary() {
} }
} }
async function renderHistory() { function renderStatsDefinition() {
if (!historyList) return if (!statsDefinition) return
if (hourlyRound?.backend !== "supabase") { if (!lastResult) {
historyList.innerHTML = '<div class="leaderboard-empty">Sign in to keep a cross-device history.</div>' statsDefinition.innerHTML = "Finish this hour's word to reveal its definition here."
return return
} }
const rows = await getPlayerHistory() if (!lastDefinition) {
if (rows.length === 0) { statsDefinition.innerHTML = "Loading definition..."
historyList.innerHTML = '<div class="leaderboard-empty">No completed rounds yet.</div>'
return return
} }
historyList.innerHTML = rows statsDefinition.innerHTML = `
.map(row => ` <span class="word-label">${lastResult.won ? "Solved word" : "Answer"}</span>
<div class="history-row"> <strong>${escapeHtml(lastDefinition.title)}</strong>
<span> <span>${escapeHtml(lastDefinition.body)}</span>
<span class="history-word">${escapeHtml(row.word)}</span> ${lastDefinition.example ? `<em>Example: ${escapeHtml(lastDefinition.example)}</em>` : ""}
<span class="history-meta">${formatHistoryHour(row.hourStart)}</span> `
</span>
<span class="history-meta">${row.won ? `${row.guessCount}/6` : "X/6"}</span>
</div>
`)
.join("")
}
async function getPlayerHistory() {
const client = getSupabaseClient()
if (!client || !authSession) return []
const { data, error } = await client.rpc("get_player_history", { history_limit: 8 })
if (error) {
console.warn("Failed to load history:", error)
return []
}
return (Array.isArray(data) ? data : []).map(row => ({
hourStart: row.hour_start,
word: row.word || "-----",
won: Boolean(row.won),
guessCount: row.guess_count || MAX_GUESSES,
completedAt: row.completed_at
}))
}
function formatHistoryHour(value) {
if (!value) return ""
return new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
hour: "numeric"
}).format(new Date(value))
} }
function renderLeaderboardLoading() { function renderLeaderboardLoading() {

View File

@ -674,37 +674,7 @@ begin
end; end;
$$; $$;
create or replace function public.get_player_history(history_limit integer default 10) drop function if exists public.get_player_history(integer);
returns table (
hour_start timestamptz,
word text,
won boolean,
guess_count integer,
completed_at timestamptz
)
language plpgsql
security definer
set search_path = public
as $$
begin
if auth.uid() is null then
raise exception 'Authentication required';
end if;
return query
select
rounds.hour_start,
rounds.word,
rounds.won,
rounds.guess_count,
rounds.completed_at
from public.wordle_rounds rounds
where rounds.user_id = auth.uid()
and rounds.completed_at is not null
order by rounds.hour_start desc
limit greatest(1, least(coalesce(history_limit, 10), 25));
end;
$$;
create or replace function public.get_hourly_summary() create or replace function public.get_hourly_summary()
returns table ( returns table (
@ -760,6 +730,5 @@ grant execute on function public.submit_guess(uuid, text) to authenticated;
grant execute on function public.get_user_stats() to authenticated; grant execute on function public.get_user_stats() to authenticated;
grant execute on function public.get_hourly_leaderboard() to anon, authenticated; grant execute on function public.get_hourly_leaderboard() to anon, authenticated;
grant execute on function public.get_leaderboard(text) to anon, authenticated; grant execute on function public.get_leaderboard(text) to anon, authenticated;
grant execute on function public.get_player_history(integer) to authenticated;
grant execute on function public.get_hourly_summary() to anon, authenticated; grant execute on function public.get_hourly_summary() to anon, authenticated;
grant execute on function public.complete_hourly_round(uuid, boolean, integer) to authenticated; grant execute on function public.complete_hourly_round(uuid, boolean, integer) to authenticated;