Update Version (Beta): login + leaderboard/ word definition
This commit is contained in:
parent
66f8207985
commit
fe5649dc2d
@ -462,43 +462,52 @@ h2 {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.history-panel {
|
||||
.definition-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.history-panel h3 {
|
||||
.definition-panel h3 {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.history-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
.stats-definition {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
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;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.history-meta {
|
||||
.stats-definition em {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.leaderboard-card {
|
||||
|
||||
@ -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.
|
||||
|
||||
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`:
|
||||
|
||||
|
||||
@ -72,9 +72,9 @@
|
||||
<div class="stats-grid" id="stats-grid"></div>
|
||||
<div class="stats-note" id="stats-note"></div>
|
||||
<div class="guess-bars" id="guess-bars" aria-label="Guess distribution"></div>
|
||||
<div class="history-panel">
|
||||
<h3>Recent history</h3>
|
||||
<div class="history-list" id="history-list"></div>
|
||||
<div class="definition-panel" id="stats-definition-panel">
|
||||
<h3>Word reveal</h3>
|
||||
<div class="stats-definition" id="stats-definition"></div>
|
||||
</div>
|
||||
<menu class="modal-actions">
|
||||
<button type="button" class="button secondary" id="reset-stats">Reset stats</button>
|
||||
|
||||
@ -72,9 +72,9 @@
|
||||
<div class="stats-grid" id="stats-grid"></div>
|
||||
<div class="stats-note" id="stats-note"></div>
|
||||
<div class="guess-bars" id="guess-bars" aria-label="Guess distribution"></div>
|
||||
<div class="history-panel">
|
||||
<h3>Recent history</h3>
|
||||
<div class="history-list" id="history-list"></div>
|
||||
<div class="definition-panel" id="stats-definition-panel">
|
||||
<h3>Word reveal</h3>
|
||||
<div class="stats-definition" id="stats-definition"></div>
|
||||
</div>
|
||||
<menu class="modal-actions">
|
||||
<button type="button" class="button secondary" id="reset-stats">Reset stats</button>
|
||||
|
||||
81
script.js
81
script.js
@ -16,6 +16,7 @@ let authSession = null
|
||||
let authProfile = null
|
||||
let leaderboardScope = "hour"
|
||||
let latestLeaderboardRows = []
|
||||
let lastDefinition = null
|
||||
|
||||
const WORD_LENGTH = 5
|
||||
const MAX_GUESSES = 6
|
||||
@ -96,7 +97,7 @@ const statsModal = document.getElementById("stats-modal")
|
||||
const statsGrid = document.getElementById("stats-grid")
|
||||
const statsNote = document.getElementById("stats-note")
|
||||
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 shareResultsButton = document.getElementById("share-results")
|
||||
|
||||
@ -419,6 +420,7 @@ function applyHourlyRound(round) {
|
||||
guesses: round.guessCount || MAX_GUESSES,
|
||||
word: round.word || ""
|
||||
}
|
||||
if (round.word) loadWordDefinition(round.word, Boolean(round.won))
|
||||
lockUntilNextWord()
|
||||
return
|
||||
}
|
||||
@ -1123,6 +1125,11 @@ 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}`)
|
||||
if (!response.ok) throw new Error(`API request failed: ${response.status}`)
|
||||
@ -1135,22 +1142,25 @@ async function showWordDefinition(word, isWin = true) {
|
||||
|
||||
if (!definition) throw new Error("No definition found")
|
||||
|
||||
showDefinitionAlert({
|
||||
lastDefinition = {
|
||||
title: `${word.toUpperCase()} ${partOfSpeech ? `(${partOfSpeech})` : ""}`,
|
||||
body: definition,
|
||||
example,
|
||||
isWin
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.info("Definition lookup unavailable:", error)
|
||||
showDefinitionAlert({
|
||||
lastDefinition = {
|
||||
title: word.toUpperCase(),
|
||||
body: "Definition not available at the moment.",
|
||||
isWin
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
renderStatsDefinition()
|
||||
return lastDefinition
|
||||
}
|
||||
|
||||
function showDefinitionAlert({ title, body, example, isWin }) {
|
||||
const alert = document.createElement("button")
|
||||
alert.type = "button"
|
||||
@ -1308,7 +1318,7 @@ async function renderStats() {
|
||||
.join("")
|
||||
|
||||
renderStatsNote(summary)
|
||||
await renderHistory()
|
||||
renderStatsDefinition()
|
||||
}
|
||||
|
||||
function renderStatsNote(summary) {
|
||||
@ -1372,60 +1382,25 @@ async function getHourlySummary() {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderHistory() {
|
||||
if (!historyList) return
|
||||
function renderStatsDefinition() {
|
||||
if (!statsDefinition) return
|
||||
|
||||
if (hourlyRound?.backend !== "supabase") {
|
||||
historyList.innerHTML = '<div class="leaderboard-empty">Sign in to keep a cross-device history.</div>'
|
||||
if (!lastResult) {
|
||||
statsDefinition.innerHTML = "Finish this hour's word to reveal its definition here."
|
||||
return
|
||||
}
|
||||
|
||||
const rows = await getPlayerHistory()
|
||||
if (rows.length === 0) {
|
||||
historyList.innerHTML = '<div class="leaderboard-empty">No completed rounds yet.</div>'
|
||||
if (!lastDefinition) {
|
||||
statsDefinition.innerHTML = "Loading definition..."
|
||||
return
|
||||
}
|
||||
|
||||
historyList.innerHTML = rows
|
||||
.map(row => `
|
||||
<div class="history-row">
|
||||
<span>
|
||||
<span class="history-word">${escapeHtml(row.word)}</span>
|
||||
<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))
|
||||
statsDefinition.innerHTML = `
|
||||
<span class="word-label">${lastResult.won ? "Solved word" : "Answer"}</span>
|
||||
<strong>${escapeHtml(lastDefinition.title)}</strong>
|
||||
<span>${escapeHtml(lastDefinition.body)}</span>
|
||||
${lastDefinition.example ? `<em>Example: ${escapeHtml(lastDefinition.example)}</em>` : ""}
|
||||
`
|
||||
}
|
||||
|
||||
function renderLeaderboardLoading() {
|
||||
|
||||
@ -674,37 +674,7 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.get_player_history(history_limit integer default 10)
|
||||
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;
|
||||
$$;
|
||||
drop function if exists public.get_player_history(integer);
|
||||
|
||||
create or replace function public.get_hourly_summary()
|
||||
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_hourly_leaderboard() 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.complete_hourly_round(uuid, boolean, integer) to authenticated;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user