More things
This commit is contained in:
@@ -9,41 +9,12 @@ const api = `${useRuntimeConfig().public.apiBase}/api`
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const { show: openSearch } = useSearch()
|
||||
|
||||
const q = ref(String(route.query.q ?? ''))
|
||||
const tag = ref(String(route.query.tag ?? ''))
|
||||
const lang = ref(String(route.query.lang ?? '').trim().toLowerCase())
|
||||
const langLabel = computed(() => (lang.value ? languageName(lang.value) : ''))
|
||||
|
||||
/* debounce the search text so we don't fetch on every keystroke */
|
||||
const debouncedQ = ref(q.value)
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
||||
watch(q, (v) => {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => (debouncedQ.value = v.trim()), 250)
|
||||
})
|
||||
|
||||
const { data: articles, status, error } = await useFetch<ArticleMeta[]>(
|
||||
`${api}/articles`,
|
||||
{ query: { q: debouncedQ, tag, lang }, default: () => [] }
|
||||
)
|
||||
|
||||
/* keep the address bar in sync with the search */
|
||||
watch([debouncedQ, tag, lang], ([qv, tv, lv]) => {
|
||||
const query: Record<string, string> = {}
|
||||
if (qv) query.q = qv
|
||||
if (tv) query.tag = tv
|
||||
if (lv) query.lang = lv
|
||||
const current = route.query
|
||||
const same
|
||||
= (current.q ?? '') === (query.q ?? '') && (current.tag ?? '') === (query.tag ?? '')
|
||||
&& (current.lang ?? '') === (query.lang ?? '')
|
||||
if (!same) router.replace({ query })
|
||||
})
|
||||
|
||||
function selectTag(t: string) {
|
||||
tag.value = tag.value === t ? '' : t
|
||||
}
|
||||
/* the bar opens the Ctrl/⌘+K popup rather than searching on the page itself,
|
||||
but it still reads back the query the address carries — the popup's
|
||||
"all results" link lands here with one */
|
||||
const askedQuery = computed(() => String(route.query.q ?? ''))
|
||||
|
||||
/* fetch one article from the API and open it */
|
||||
const randomPending = ref(false)
|
||||
@@ -52,7 +23,14 @@ async function showRandomArticle() {
|
||||
randomPending.value = true
|
||||
try {
|
||||
const article = await $fetch<ArticleMeta>(`${api}/articles/random`)
|
||||
await router.push(versionHref(identityOf(article), article))
|
||||
/* the canonical slug is the article's address, and the middleware there
|
||||
turns it onto the reader's language; an orphaned translation answers to
|
||||
no such address, so that one still goes straight to its own version */
|
||||
await router.push(
|
||||
article.baseSlug
|
||||
? versionHref(identityOf(article), articleLanguage(article))
|
||||
: `/wiki/${encodeURIComponent(article.slug)}`
|
||||
)
|
||||
} catch {
|
||||
// no articles yet or API unreachable — the banner below already covers API issues
|
||||
} finally {
|
||||
@@ -60,38 +38,14 @@ async function showRandomArticle() {
|
||||
}
|
||||
}
|
||||
|
||||
/* "/" focuses the search box */
|
||||
const searchEl = ref<HTMLInputElement | null>(null)
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
const typing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement
|
||||
if (e.key === '/' && !typing) {
|
||||
e.preventDefault()
|
||||
searchEl.value?.focus()
|
||||
}
|
||||
}
|
||||
onMounted(() => window.addEventListener('keydown', onKeydown))
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
|
||||
|
||||
useSeoMeta({
|
||||
title: () => t('home.metaTitle'),
|
||||
description: () => t('home.metaDescription'),
|
||||
})
|
||||
|
||||
/* which empty-shelf line to say, depending on the filters in force */
|
||||
const emptyKey = computed(() =>
|
||||
tag.value && lang.value
|
||||
? 'home.emptyTagLang'
|
||||
: tag.value
|
||||
? 'home.emptyTag'
|
||||
: lang.value
|
||||
? 'home.emptyLang'
|
||||
: 'home.emptyPlain'
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-main">
|
||||
<div class="home-main is-home">
|
||||
<section class="hero">
|
||||
<h1 v-html="t('home.hero')"></h1>
|
||||
|
||||
@@ -100,14 +54,14 @@ const emptyKey = computed(() =>
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="m20 20-3.2-3.2" />
|
||||
</svg>
|
||||
<input
|
||||
ref="searchEl"
|
||||
v-model="q"
|
||||
class="search-input"
|
||||
type="text"
|
||||
:placeholder="t('home.searchPlaceholder')"
|
||||
<button
|
||||
type="button"
|
||||
class="search-input search-trigger"
|
||||
:aria-label="t('common.searchArticles')"
|
||||
/>
|
||||
@click="openSearch()"
|
||||
>
|
||||
{{ askedQuery || t('home.searchPlaceholder') }}
|
||||
</button>
|
||||
<span class="search-kbd">/</span>
|
||||
</div>
|
||||
|
||||
@@ -123,52 +77,5 @@ const emptyKey = computed(() =>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="results-bar">
|
||||
<h2>
|
||||
<template v-if="debouncedQ || tag || lang">
|
||||
{{ t('home.results', articles.length) }}
|
||||
</template>
|
||||
<template v-else>{{ t('home.recentlyUpdated') }}</template>
|
||||
</h2>
|
||||
<span v-if="tag" class="filter-chip">
|
||||
{{ t('common.tagChip', { tag }) }}
|
||||
<button type="button" :aria-label="t('common.clearTag')" @click="tag = ''">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<path d="M6 6l12 12M18 6L6 18" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="lang" class="filter-chip">
|
||||
{{ t('common.languageChip', { lang: langLabel }) }}
|
||||
<button type="button" :aria-label="t('common.clearLanguage')" @click="lang = ''">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<path d="M6 6l12 12M18 6L6 18" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="banner">
|
||||
<span class="glyph">!</span>
|
||||
<span v-html="t('common.apiDown')"></span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status === 'pending'" class="articles-grid">
|
||||
<div v-for="i in 6" :key="i" class="skeleton" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="articles.length" class="articles-grid">
|
||||
<ArticleCard v-for="a in articles" :key="a._id" :article="a" />
|
||||
</div>
|
||||
|
||||
<div v-else class="empty">
|
||||
<div class="glyph">∅</div>
|
||||
<h3>{{ t('home.emptyTitle') }}</h3>
|
||||
<p>{{ t(emptyKey, { tag, lang: langLabel }) }}</p>
|
||||
<NuxtLink to="/new" class="btn btn-primary">{{ t('common.startWriting') }}</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user