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;
|
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 {
|
||||||
|
|||||||
@ -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`:
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
81
script.js
81
script.js
@ -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() {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user