Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions pages/[lang]/word/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,38 @@ function shareWord() {
const asyncDef = ref<any>(null);
const showAsyncDef = ref(false);

// Client-side: reveal today's word if game is over
const todayRevealed = ref<string | null>(null);
const todayRevealedDef = ref<any>(null);

onMounted(async () => {
// Check if today's word should be revealed (game over)
if (d.is_today && !word) {
try {
const saved = localStorage.getItem(lang);
if (saved) {
const state = JSON.parse(saved);
if (state.game_over && state.todays_word) {
todayRevealed.value = state.todays_word;
// Fetch definition for the revealed word
try {
const defData = await $fetch(
`/api/${lang}/definition/${encodeURIComponent(state.todays_word)}`
);
if (defData && (defData as any).definition) {
todayRevealedDef.value = defData;
}
} catch {
// definition not available
}
}
}
} catch {
// localStorage unavailable
}
return;
}

if (!word || (definition && definition.definition)) return;
try {
const data = await $fetch(`/api/${lang}/definition/${word}`);
Expand Down Expand Up @@ -283,8 +314,8 @@ onMounted(() => {
</div>
</template>

<!-- Today's word -->
<template v-else-if="d.is_today">
<!-- Today's word: not yet played -->
<template v-else-if="d.is_today && !todayRevealed">
<div class="text-center py-8">
<p class="text-lg font-bold text-green-700 dark:text-green-400 mb-2">
Today's word!
Expand All @@ -301,6 +332,52 @@ onMounted(() => {
</div>
</template>

<!-- Today's word: revealed (game over) -->
<template v-else-if="d.is_today && todayRevealed">
<!-- Word Tiles -->
<div class="flex justify-center gap-1.5 mb-4">
<div
v-for="(letter, li) in todayRevealed"
:key="li"
class="w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center text-xl sm:text-2xl font-bold text-white bg-green-500 rounded-md shadow-sm uppercase"
>
{{ letter }}
</div>
</div>

<!-- Definition -->
<div
v-if="todayRevealedDef && todayRevealedDef.definition"
class="bg-neutral-50 dark:bg-neutral-800 rounded-lg p-4 mb-4"
>
<div class="flex items-center gap-2 mb-1">
<span
class="text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400"
>
Definition
</span>
<span
v-if="todayRevealedDef.part_of_speech"
class="text-xs text-neutral-400 dark:text-neutral-500 italic"
>
{{ todayRevealedDef.part_of_speech }}
</span>
</div>
<p class="text-sm text-neutral-800 dark:text-neutral-200">
<strong class="uppercase">{{ todayRevealed }}</strong> &mdash;
{{ todayRevealedDef.definition_native || todayRevealedDef.definition }}
</p>
<a
:href="`https://${wiktLang}.wiktionary.org/wiki/${todayRevealed}`"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 mt-2 text-xs text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
>
Wiktionary &#8599;
</a>
</div>
</template>

<!-- Past word (main content) -->
<template v-else-if="word">
<!-- AI Word Art Image -->
Expand Down
76 changes: 73 additions & 3 deletions pages/[lang]/words.vue
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,44 @@ function formatDate(dateStr: string): string {
function winRate(stats: { total: number; wins: number }): number {
return Math.round((stats.wins / stats.total) * 100);
}

// Client-side: reveal today's word if game is over (won or lost)
const todayRevealed = ref<string | null>(null);
const completedDays = ref(new Set<number>());

onMounted(() => {
try {
// Check current game state — key is the language code
const saved = localStorage.getItem(lang);
if (saved) {
const state = JSON.parse(saved);
if (state.game_over && state.todays_word) {
todayRevealed.value = state.todays_word;
}
}

// Build set of completed day indices from game_results
const results = localStorage.getItem('game_results');
if (results) {
const parsed = JSON.parse(results);
const langResults = parsed[lang];
if (Array.isArray(langResults)) {
// game_results stores [{won, attempts, date}, ...]
// We need to map dates to day indices
for (const r of langResults) {
if (r.date) {
const d = new Date(r.date);
const nDays = Math.floor(d.getTime() / 86400000);
const idx = nDays - 18992 + 195;
completedDays.value.add(idx);
}
}
}
}
} catch {
// localStorage unavailable
}
});
</script>

<template>
Expand All @@ -172,9 +210,9 @@ function winRate(stats: { total: number; wins: number }): number {
<!-- Word Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<template v-for="w in words" :key="w.day_idx">
<!-- Today's word: mystery card -->
<!-- Today's word: revealed if game over, mystery if not -->
<NuxtLink
v-if="w.is_today"
v-if="w.is_today && !todayRevealed"
:to="`/${lang}`"
class="block bg-neutral-50 dark:bg-neutral-800 rounded-lg p-4 hover:shadow-md hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-all group border-2 border-dashed border-green-500/40"
>
Expand All @@ -197,12 +235,44 @@ function winRate(stats: { total: number; wins: number }): number {
</p>
</NuxtLink>

<!-- Today's word: revealed (game over) -->
<NuxtLink
v-else-if="w.is_today && todayRevealed"
:to="`/${lang}/word/${w.day_idx}`"
class="block bg-neutral-50 dark:bg-neutral-800 rounded-lg p-4 hover:shadow-md hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-all group border-2 border-green-500/40"
>
<div class="flex justify-center gap-1 mb-2">
<div
v-for="(letter, li) in todayRevealed"
:key="li"
class="w-8 h-8 flex items-center justify-center text-sm font-bold text-white bg-green-500 rounded uppercase"
>
{{ letter }}
</div>
</div>
<p class="text-xs text-neutral-400 text-center">
#{{ w.day_idx }} &middot; {{ formatDate(w.date) }}
</p>
<p
class="text-sm font-semibold text-green-600 dark:text-green-400 mt-1 text-center"
>
Today
</p>
</NuxtLink>

<!-- Past word: normal card -->
<NuxtLink
v-else
:to="`/${lang}/word/${w.day_idx}`"
class="block bg-neutral-50 dark:bg-neutral-800 rounded-lg p-4 hover:shadow-md hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-all group"
class="block bg-neutral-50 dark:bg-neutral-800 rounded-lg p-4 hover:shadow-md hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-all group relative"
>
<!-- Played checkmark -->
<span
v-if="completedDays.has(w.day_idx)"
class="absolute top-2 right-2 text-green-500 dark:text-green-400 text-xs"
title="Played"
>&#10003;</span
>
<!-- Word tiles (mini) -->
<div class="flex justify-center gap-1 mb-2">
<div
Expand Down
7 changes: 4 additions & 3 deletions server/api/[lang]/definition/[word].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 404, message: 'Unknown language' });
}

// Only serve definitions for valid words
const wordLower = word.toLowerCase();
if (!data.wordLists[lang]!.includes(wordLower)) {
// Only serve definitions for valid words (normalize to NFC for consistent matching)
const wordLower = word.toLowerCase().normalize('NFC');
const wordList = data.wordLists[lang]!;
if (!wordList.includes(wordLower) && !wordList.includes(wordLower.normalize('NFD'))) {
throw createError({ statusCode: 404, message: 'Unknown word' });
}

Expand Down
2 changes: 1 addition & 1 deletion server/api/[lang]/word-image/[word].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default defineEventHandler(async (event) => {
}

const expectedWord = getWordForDay(lang, dayIdx);
if (word.toLowerCase() !== expectedWord.toLowerCase()) {
if (word.toLowerCase().normalize('NFC') !== expectedWord.toLowerCase().normalize('NFC')) {
throw createError({ statusCode: 403, message: 'Not a valid daily word' });
}

Expand Down
Loading