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
155 changes: 92 additions & 63 deletions pages/[lang]/words.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ useHead({
mainEntity: {
'@type': 'ItemList',
numberOfItems: words.value.length,
itemListElement: words.value.map((w, i) => ({
'@type': 'ListItem',
position: i + 1 + (page.value - 1) * 30,
url: `https://wordle.global/${lang}/word/${w.day_idx}`,
name: `${w.word.toUpperCase()} \u2014 Wordle ${langNameNative.value} #${w.day_idx}`,
})),
itemListElement: words.value
.filter((w) => w.word)
.map((w, i) => ({
'@type': 'ListItem',
position: i + 1 + (page.value - 1) * 30,
url: `https://wordle.global/${lang}/word/${w.day_idx}`,
name: `${w.word!.toUpperCase()} \u2014 Wordle ${langNameNative.value} #${w.day_idx}`,
})),
},
})
),
Expand Down Expand Up @@ -169,69 +171,96 @@ 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">
<NuxtLink
v-for="w in words"
:key="w.day_idx"
: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"
>
<!-- Word tiles (mini) -->
<div class="flex justify-center gap-1 mb-2">
<div
v-for="(letter, li) in w.word"
:key="li"
class="w-8 h-8 flex items-center justify-center text-sm font-bold text-white bg-green-500 rounded uppercase"
<template v-for="w in words" :key="w.day_idx">
<!-- Today's word: mystery card -->
<NuxtLink
v-if="w.is_today"
: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"
>
<div class="flex justify-center gap-1 mb-2">
<div
v-for="i in 5"
:key="i"
class="w-8 h-8 flex items-center justify-center text-sm font-bold text-white bg-neutral-400 dark:bg-neutral-600 rounded"
>
?
</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-2 text-center"
>
{{ letter }}
Today's word &mdash; Play to reveal!
</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"
>
<!-- Word tiles (mini) -->
<div class="flex justify-center gap-1 mb-2">
<div
v-for="(letter, li) in w.word"
: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>
</div>

<!-- Day and date -->
<p class="text-xs text-neutral-400 text-center">
#{{ w.day_idx }} &middot; {{ formatDate(w.date) }}
</p>
<!-- Day and date -->
<p class="text-xs text-neutral-400 text-center">
#{{ w.day_idx }} &middot; {{ formatDate(w.date) }}
</p>

<!-- Definition snippet -->
<p
v-if="w.definition && w.definition.definition"
class="text-xs text-neutral-600 dark:text-neutral-300 mt-1 line-clamp-2 text-center"
>
{{
w.definition.definition.length > 80
? w.definition.definition.slice(0, 80) + '\u2026'
: w.definition.definition
}}
</p>
<!-- Definition snippet -->
<p
v-if="w.definition && w.definition.definition"
class="text-xs text-neutral-600 dark:text-neutral-300 mt-1 line-clamp-2 text-center"
>
{{
w.definition.definition.length > 80
? w.definition.definition.slice(0, 80) + '\u2026'
: w.definition.definition
}}
</p>

<!-- Stats summary -->
<div
v-if="w.stats && w.stats.total > 0"
class="flex justify-center gap-3 mt-2 text-[10px] text-neutral-400"
>
<span>{{ w.stats.total }} plays</span>
<span>{{ winRate(w.stats) }}% win</span>
</div>
<!-- Stats summary -->
<div
v-if="w.stats && w.stats.total > 0"
class="flex justify-center gap-3 mt-2 text-[10px] text-neutral-400"
>
<span>{{ w.stats.total }} plays</span>
<span>{{ winRate(w.stats) }}% win</span>
</div>

<!-- AI art thumbnail (loads async) -->
<div class="mt-2 overflow-hidden rounded hidden" :data-img-id="w.day_idx">
<img
:src="`/api/${lang}/word-image/${w.word}?day_idx=${w.day_idx}`"
:alt="w.word"
class="w-full h-24 object-cover"
loading="lazy"
@load="
($event.target as HTMLImageElement).parentElement!.classList.remove(
'hidden'
)
"
@error="
($event.target as HTMLImageElement).parentElement!.classList.add(
'hidden'
)
"
/>
</div>
</NuxtLink>
<!-- AI art thumbnail (loads async) -->
<div class="mt-2 overflow-hidden rounded hidden" :data-img-id="w.day_idx">
<img
:src="`/api/${lang}/word-image/${w.word}?day_idx=${w.day_idx}`"
:alt="w.word || ''"
class="w-full h-24 object-cover"
loading="lazy"
@load="
(
$event.target as HTMLImageElement
).parentElement!.classList.remove('hidden')
"
@error="
(
$event.target as HTMLImageElement
).parentElement!.classList.add('hidden')
"
/>
</div>
</NuxtLink>
</template>
</div>

<!-- Pagination -->
Expand Down
2 changes: 1 addition & 1 deletion server/api/[lang]/definition/[word].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fetchDefinition } from '../../../utils/definitions';

export default defineEventHandler(async (event) => {
const lang = getRouterParam(event, 'lang')!;
const word = getRouterParam(event, 'word')!;
const word = getRouterParam(event, 'word')!.normalize('NFC');
const data = loadAllData();

if (!data.languageCodes.includes(lang)) {
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 @@ -47,7 +47,7 @@ const IMAGE_MIN_DAY_IDX = 1708;

export default defineEventHandler(async (event) => {
const lang = getRouterParam(event, 'lang')!;
const word = getRouterParam(event, 'word')!;
const word = getRouterParam(event, 'word')!.normalize('NFC');
const data = loadAllData();

if (!data.languageCodes.includes(lang)) {
Expand Down
11 changes: 7 additions & 4 deletions server/api/[lang]/words.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,27 @@ export default defineEventHandler((event) => {

const words: Array<{
day_idx: number;
word: string;
word: string | null;
date: string;
definition: { definition: string; part_of_speech?: string } | null;
stats: { total: number; wins: number } | null;
is_today: boolean;
}> = [];

for (let idx = startIdx; idx > endIdx; idx--) {
const isToday = idx === todaysIdx;
const word = getWordForDay(lang, idx);
const date = idxToDate(idx);
const stats = loadWordStats(lang, idx);
const defResult = readCachedDefinition(word, lang);
const stats = isToday ? null : loadWordStats(lang, idx);
const defResult = isToday ? null : readCachedDefinition(word, lang);

words.push({
day_idx: idx,
word,
word: isToday ? null : word,
date: date.toISOString().slice(0, 10),
definition: defResult,
stats: stats ? { total: stats.total, wins: stats.wins } : null,
is_today: isToday,
});
}

Expand Down
17 changes: 12 additions & 5 deletions tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,18 @@ describe('API Routes', () => {
expect(data.words.length).toBeGreaterThan(0);
expect(data.words.length).toBeLessThanOrEqual(30);

// Check word entry shape
const word = data.words[0];
expect(word.day_idx).toBeDefined();
expect(typeof word.word).toBe('string');
expect(word.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
// First entry is today's word (hidden)
const todayWord = data.words[0];
expect(todayWord.day_idx).toBeDefined();
expect(todayWord.is_today).toBe(true);
expect(todayWord.word).toBeNull();

// Second entry is a past word (visible)
if (data.words.length > 1) {
const pastWord = data.words[1];
expect(typeof pastWord.word).toBe('string');
expect(pastWord.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
}
});
});

Expand Down
Loading