From 28dc9001da8f674ffa4505c15f63a43144bc4a21 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 17 Mar 2018 20:41:18 +0100 Subject: [PATCH] Make search great again --- pages/index.go | 6 ++ pages/search/search.go | 64 +++++++++++++++++++- pages/search/search.pixy | 15 +++-- scripts/Actions/Search.ts | 121 +++++++++++++++++++++++++++++++++++--- scripts/AnimeNotifier.ts | 4 ++ 5 files changed, 196 insertions(+), 14 deletions(-) diff --git a/pages/index.go b/pages/index.go index 57c51a74..8b72df73 100644 --- a/pages/index.go +++ b/pages/index.go @@ -205,6 +205,12 @@ func Configure(app *aero.Application) { // Search l.Page("/search/*term", search.Get) + l.Page("/empty-search", search.GetEmptySearch) + l.Page("/anime-search/*term", search.Anime) + l.Page("/character-search/*term", search.Characters) + l.Page("/forum-search/*term", search.Forum) + l.Page("/soundtrack-search/*term", search.SoundTracks) + l.Page("/user-search/*term", search.Users) // Shop l.Page("/support", support.Get) diff --git a/pages/search/search.go b/pages/search/search.go index e446bd79..1ed296f6 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -3,6 +3,8 @@ package search import ( "strings" + "github.com/aerogo/flow" + "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" @@ -12,7 +14,7 @@ const maxUsers = 25 const maxAnime = 25 const maxPosts = 2 const maxThreads = 2 -const maxTracks = 4 +const maxSoundTracks = 4 const maxCharacters = 22 // Get search page. @@ -20,6 +22,64 @@ func Get(ctx *aero.Context) string { term := ctx.Get("term") term = strings.TrimPrefix(term, "/") - users, animes, posts, threads, tracks, characters := arn.Search(term, maxUsers, maxAnime, maxPosts, maxThreads, maxTracks, maxCharacters) + users, animes, posts, threads, tracks, characters := arn.Search(term, maxUsers, maxAnime, maxPosts, maxThreads, maxSoundTracks, maxCharacters) return ctx.HTML(components.SearchResults(term, users, animes, posts, threads, tracks, characters)) } + +// GetEmptySearch renders the search page with no contents. +func GetEmptySearch(ctx *aero.Context) string { + return ctx.HTML(components.SearchResults("", nil, nil, nil, nil, nil, nil)) +} + +// Anime search. +func Anime(ctx *aero.Context) string { + term := ctx.Get("term") + term = strings.TrimPrefix(term, "/") + + animes := arn.SearchAnime(term, maxAnime) + return ctx.HTML(components.AnimeSearchResults(animes)) +} + +// Characters search. +func Characters(ctx *aero.Context) string { + term := ctx.Get("term") + term = strings.TrimPrefix(term, "/") + + characters := arn.SearchCharacters(term, maxCharacters) + return ctx.HTML(components.CharacterSearchResults(characters)) +} + +// Forum search. +func Forum(ctx *aero.Context) string { + term := ctx.Get("term") + term = strings.TrimPrefix(term, "/") + + var posts []*arn.Post + var threads []*arn.Thread + + flow.Parallel(func() { + posts = arn.SearchPosts(term, maxPosts) + }, func() { + threads = arn.SearchThreads(term, maxThreads) + }) + + return ctx.HTML(components.ForumSearchResults(posts, threads)) +} + +// SoundTracks search. +func SoundTracks(ctx *aero.Context) string { + term := ctx.Get("term") + term = strings.TrimPrefix(term, "/") + + tracks := arn.SearchSoundTracks(term, maxSoundTracks) + return ctx.HTML(components.SoundTrackSearchResults(tracks)) +} + +// Users search. +func Users(ctx *aero.Context) string { + term := ctx.Get("term") + term = strings.TrimPrefix(term, "/") + + users := arn.SearchUsers(term, maxUsers) + return ctx.HTML(components.UserSearchResults(users)) +} diff --git a/pages/search/search.pixy b/pages/search/search.pixy index cfb13ddf..7786a6be 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -7,35 +7,40 @@ component SearchResults(term string, users []*arn.User, animes []*arn.Anime, pos Icon("tv") span Anime - AnimeSearchResults(animes) + #anime-search-results + AnimeSearchResults(animes) .widget h3.widget-title Icon("user") span Characters - CharacterSearchResults(characters) + #character-search-results + CharacterSearchResults(characters) .widget h3.widget-title Icon("comment") span Forum - ForumSearchResults(posts, threads) + #forum-search-results + ForumSearchResults(posts, threads) .widget h3.widget-title Icon("music") span Soundtracks - SoundTrackSearchResults(tracks) + #soundtrack-search-results + SoundTrackSearchResults(tracks) .widget h3.widget-title Icon("user") span Users - UserSearchResults(users) + #user-search-results + UserSearchResults(users) component AnimeSearchResults(animes []*arn.Anime) if len(animes) == 0 diff --git a/scripts/Actions/Search.ts b/scripts/Actions/Search.ts index 69eba5d8..e5f2bb96 100644 --- a/scripts/Actions/Search.ts +++ b/scripts/Actions/Search.ts @@ -1,19 +1,126 @@ import { AnimeNotifier } from "../AnimeNotifier" +// Search page reference +var emptySearchHTML = "" +var searchPage: HTMLElement +var correctResponseRendered = { + "anime": false, + "character": false, + "forum": false, + "soundtrack": false, + "user": false +} + +// Containers for all the search results +var animeSearchResults: HTMLElement +var characterSearchResults: HTMLElement +var forumSearchResults: HTMLElement +var soundtrackSearchResults: HTMLElement +var userSearchResults: HTMLElement + // Search -export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { +export async function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { if(e.ctrlKey || e.altKey) { return } - let term = search.value + let term = search.value.trim() + let searchPageActivated = (searchPage === arn.app.content.children[0]) - if(!term || term.length < 1) { - arn.app.content.innerHTML = "Please enter at least 1 character to start searching." - return + // Reset + correctResponseRendered.anime = false + correctResponseRendered.character = false + correctResponseRendered.forum = false + correctResponseRendered.soundtrack = false + correctResponseRendered.user = false + + // Set browser URL + let url = "/search/" + term + history.pushState(url, null, url) + arn.app.currentPath = url + + // Unmount mountables to improve visual responsiveness on key press + arn.unmountMountables() + + // Show loading spinner + arn.loading(true) + + try { + // Fetch empty search frame if needed + if(emptySearchHTML === "") { + let response = await fetch("/_/empty-search") + emptySearchHTML = await response.text() + } + + if(!searchPageActivated) { + if(!searchPage) { + searchPage = document.createElement("div") + searchPage.innerHTML = emptySearchHTML + } + + arn.app.content.innerHTML = "" + arn.app.content.appendChild(searchPage) + } + + if(!animeSearchResults) { + animeSearchResults = document.getElementById("anime-search-results") + characterSearchResults = document.getElementById("character-search-results") + forumSearchResults = document.getElementById("forum-search-results") + soundtrackSearchResults = document.getElementById("soundtrack-search-results") + userSearchResults = document.getElementById("user-search-results") + } + + if(!term || term.length < 1) { + await arn.innerHTML(searchPage, emptySearchHTML) + arn.app.emit("DOMContentLoaded") + return + } + + // Start searching + fetch("/_/anime-search/" + term) + .then(showResponseInElement(arn, url, "anime", animeSearchResults)) + .catch(console.error) + + fetch("/_/character-search/" + term) + .then(showResponseInElement(arn, url, "character", characterSearchResults)) + .catch(console.error) + + fetch("/_/forum-search/" + term) + .then(showResponseInElement(arn, url, "forum", forumSearchResults)) + .catch(console.error) + + fetch("/_/soundtrack-search/" + term) + .then(showResponseInElement(arn, url, "soundtrack", soundtrackSearchResults)) + .catch(console.error) + + fetch("/_/user-search/" + term) + .then(showResponseInElement(arn, url, "user", userSearchResults)) + .catch(console.error) + } catch(err) { + console.error(err) + } finally { + arn.loading(false) } +} - arn.diff("/search/" + term) +function showResponseInElement(arn: AnimeNotifier, url: string, typeName: string, element: HTMLElement) { + return async response => { + let html = await response.text() + + if(arn.app.currentPath !== url) { + // Return if this result would overwrite the already arrived correct result + if(correctResponseRendered[typeName]) { + return + } + } else { + correctResponseRendered[typeName] = true + } + + await arn.innerHTML(element, html) + + // Emit content loaded event + arn.app.emit("DOMContentLoaded") + } } // Search database @@ -67,7 +174,7 @@ export function searchDB(arn: AnimeNotifier, input: HTMLInputElement, e: Keyboar } else { link.href = "/" + dataType.toLowerCase() + "/" + record.id } - + link.target = "_blank" container.appendChild(link) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index f6918447..9b26a445 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -710,6 +710,10 @@ export class AnimeNotifier { } } + innerHTML(element: HTMLElement, html: string) { + return Diff.innerHTML(element, html) + } + post(url: string, body: any) { if(this.isLoading) { return Promise.resolve(null)