Added search to tags page.
This commit is contained in:
parent
ed4bb8e09d
commit
055ca3a376
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
src="/public/img/icons/movie.svg"
|
src="/img/icons/movie.svg"
|
||||||
class="nocover"
|
class="nocover"
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<input
|
<input
|
||||||
v-model="query"
|
v-model="query"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search channel"
|
placeholder="Search channels"
|
||||||
class="search input"
|
class="search input"
|
||||||
@search="search"
|
@search="search"
|
||||||
>
|
>
|
||||||
|
@ -118,6 +118,10 @@ async function search() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
|
@ -2,8 +2,19 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<ul
|
<ul
|
||||||
ref="categories"
|
ref="categories"
|
||||||
class="categories nolist"
|
class="categories nolist nobar"
|
||||||
>
|
>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="search noselect"
|
||||||
|
@click="focusSearch"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
v-for="(tags, category) in showcase"
|
v-for="(tags, category) in showcase"
|
||||||
:key="`category-${category}`"
|
:key="`category-${category}`"
|
||||||
|
@ -12,7 +23,7 @@
|
||||||
:href="`#${category}`"
|
:href="`#${category}`"
|
||||||
class="category nolink"
|
class="category nolink"
|
||||||
:class="{ active: activeCategory === category }"
|
:class="{ active: activeCategory === category }"
|
||||||
>{{ category }}</a>
|
>{{ categoryTitles[category] || category }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -20,6 +31,25 @@
|
||||||
ref="content"
|
ref="content"
|
||||||
class="content"
|
class="content"
|
||||||
>
|
>
|
||||||
|
<form
|
||||||
|
class="search-container"
|
||||||
|
@submit.prevent="search"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
v-model="query"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search tags"
|
||||||
|
class="search input"
|
||||||
|
@search="search"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="search"
|
||||||
|
@click="search"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(tags, category) in showcase"
|
v-for="(tags, category) in showcase"
|
||||||
:key="`tags-${category}`"
|
:key="`tags-${category}`"
|
||||||
|
@ -41,7 +71,7 @@
|
||||||
<div class="thumb-container">
|
<div class="thumb-container">
|
||||||
<a
|
<a
|
||||||
:href="`/tag/${tag.slug}`"
|
:href="`/tag/${tag.slug}`"
|
||||||
class="tag nolink"
|
class="thumb-link nolink"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="tag.poster"
|
v-if="tag.poster"
|
||||||
|
@ -51,6 +81,12 @@
|
||||||
class="thumb"
|
class="thumb"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="/img/icons/price-tag2.svg"
|
||||||
|
class="nophoto"
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
@ -80,13 +116,17 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, inject } from 'vue';
|
import { ref, onMounted, inject } from 'vue';
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
import navigate from '#/src/navigate.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const showcase = pageContext.pageProps.tagShowcase;
|
const showcase = pageContext.pageProps.tagShowcase;
|
||||||
|
|
||||||
const categories = ref(null);
|
const categories = ref(null);
|
||||||
const content = ref(null);
|
const content = ref(null);
|
||||||
|
const searchInput = ref(null);
|
||||||
|
const query = ref(pageContext.urlParsed.search.q);
|
||||||
|
|
||||||
const categoryTitles = {
|
const categoryTitles = {
|
||||||
lgbt: 'LGBT',
|
lgbt: 'LGBT',
|
||||||
|
@ -120,15 +160,16 @@ function calculateActiveCategory() {
|
||||||
navigate(`#${activeCategory.value}`, null, { replace: true });
|
navigate(`#${activeCategory.value}`, null, { replace: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
navigate('/tags', { q: query.value || undefined }, { redirect: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSearch() {
|
||||||
|
events.emit('scrollUp'); // scrollIntoView on search input does not reveal it fully
|
||||||
|
searchInput.value?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// div doesn't scroll automatically on page load, reset hash to scroll
|
|
||||||
if (window.location.hash) {
|
|
||||||
const hash = window.location.hash;
|
|
||||||
|
|
||||||
window.location.hash = undefined;
|
|
||||||
window.location.hash = hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.value.addEventListener('wheel', (event) => {
|
categories.value.addEventListener('wheel', (event) => {
|
||||||
categories.value.scrollLeft += event.deltaY;
|
categories.value.scrollLeft += event.deltaY;
|
||||||
});
|
});
|
||||||
|
@ -144,7 +185,15 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
calculateActiveCategory();
|
// div doesn't scroll automatically on page load, reset hash to scroll
|
||||||
|
if (window.location.hash) {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
|
||||||
|
window.location.hash = undefined;
|
||||||
|
window.location.hash = hash;
|
||||||
|
|
||||||
|
calculateActiveCategory();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -152,6 +201,7 @@ onMounted(() => {
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-grow:1 ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@ -162,16 +212,26 @@ onMounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .25rem;
|
gap: .25rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: .5rem 1rem;
|
padding: .5rem 1rem .5rem 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: var(--grey-dark-40);
|
background: var(--grey-dark-40);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
.search {
|
||||||
display: none;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--highlight-strong-20);
|
||||||
|
padding: 0 .5rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .icon {
|
||||||
|
fill: var(--text-light);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,6 +250,27 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 1rem 1rem 0 1rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: 0 1rem;
|
||||||
|
height: auto;
|
||||||
|
fill: var(--shadow);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
|
@ -205,6 +286,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
@ -220,7 +302,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
padding: .25rem .5rem 0 .5rem;
|
padding: .4rem .5rem 0 .5rem;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -229,17 +311,31 @@ onMounted(() => {
|
||||||
|
|
||||||
.thumb-container {
|
.thumb-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
aspect-ratio: 5/3;
|
||||||
|
background: var(--background-base-20);
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-link {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 5/3;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
}
|
||||||
|
|
||||||
|
.nophoto {
|
||||||
|
width: 10%;
|
||||||
|
opacity: .1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favicon-link {
|
.favicon-link {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { fetchTagsById } from '#/src/tags.js';
|
import { fetchTags, fetchTagsById } from '#/src/tags.js';
|
||||||
|
|
||||||
const tagSlugs = {
|
const tagSlugs = {
|
||||||
popular: [
|
popular: [
|
||||||
|
@ -116,8 +116,28 @@ const tagSlugs = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function searchTags(pageContext) {
|
||||||
|
const tags = await fetchTags({ query: pageContext.urlParsed.search.q });
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
title: 'Tags',
|
||||||
|
pageProps: {
|
||||||
|
tagShowcase: {
|
||||||
|
results: tags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
|
if (pageContext.urlParsed.search.q) {
|
||||||
|
return searchTags(pageContext);
|
||||||
|
}
|
||||||
|
|
||||||
const tags = await fetchTagsById(Object.values(tagSlugs).flat());
|
const tags = await fetchTagsById(Object.values(tagSlugs).flat());
|
||||||
|
|
||||||
const filteredTags = tags.filter((tag) => !pageContext.tagFilter.includes(tag.name) && !pageContext.tagFilter.includes(tag.slug));
|
const filteredTags = tags.filter((tag) => !pageContext.tagFilter.includes(tag.name) && !pageContext.tagFilter.includes(tag.slug));
|
||||||
const tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag]));
|
const tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag]));
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
|
||||||
|
<path d="M15.25 0h-6c-0.412 0-0.989 0.239-1.28 0.53l-7.439 7.439c-0.292 0.292-0.292 0.769 0 1.061l6.439 6.439c0.292 0.292 0.769 0.292 1.061 0l7.439-7.439c0.292-0.292 0.53-0.868 0.53-1.28v-6c0-0.412-0.338-0.75-0.75-0.75zM11.5 6c-0.828 0-1.5-0.672-1.5-1.5s0.672-1.5 1.5-1.5 1.5 0.672 1.5 1.5-0.672 1.5-1.5 1.5z"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 425 B |
30
src/tags.js
30
src/tags.js
|
@ -30,25 +30,37 @@ function curateTag(tag, context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTags(options = {}) {
|
export async function fetchTags(options = {}) {
|
||||||
|
const query = options.query?.trim();
|
||||||
|
|
||||||
const [tags, posters] = await Promise.all([
|
const [tags, posters] = await Promise.all([
|
||||||
knex('tags')
|
knex('tags')
|
||||||
.select('tags.*')
|
.select('aliases.*')
|
||||||
|
.leftJoin(knex.raw('tags AS aliases ON aliases.id = tags.alias_for OR (tags.alias_for IS NULL AND aliases.id = tags.id)'))
|
||||||
.modify((builder) => {
|
.modify((builder) => {
|
||||||
if (!options.includeAliases) {
|
if (query) {
|
||||||
builder.whereNull('alias_for');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.query) {
|
|
||||||
builder
|
builder
|
||||||
.whereILike('name', `%${options.query}%`)
|
.whereILike('tags.name', `%${query}%`)
|
||||||
.orWhereILike('slug', `%${options.query}%`);
|
.orWhereILike('tags.slug', `%${query}%`)
|
||||||
|
.groupBy('aliases.id')
|
||||||
|
.orderBy([
|
||||||
|
{
|
||||||
|
column: knex.raw('similarity(aliases.slug, :query)', { query }),
|
||||||
|
order: 'desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'aliases.slug',
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else if (!options.includeAliases) {
|
||||||
|
builder.whereNull('alias_for');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
knex('tags_posters')
|
knex('tags_posters')
|
||||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||||
.leftJoin('entities as parents', 'entities.id', 'entities.parent_id'),
|
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
|
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
|
||||||
|
|
Loading…
Reference in New Issue