Make search great again

This commit is contained in:
Eduard Urbach 2018-03-17 20:41:18 +01:00
parent b21329538c
commit 28dc9001da
5 changed files with 196 additions and 14 deletions

View File

@ -205,6 +205,12 @@ func Configure(app *aero.Application) {
// Search // Search
l.Page("/search/*term", search.Get) 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 // Shop
l.Page("/support", support.Get) l.Page("/support", support.Get)

View File

@ -3,6 +3,8 @@ package search
import ( import (
"strings" "strings"
"github.com/aerogo/flow"
"github.com/aerogo/aero" "github.com/aerogo/aero"
"github.com/animenotifier/arn" "github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/components"
@ -12,7 +14,7 @@ const maxUsers = 25
const maxAnime = 25 const maxAnime = 25
const maxPosts = 2 const maxPosts = 2
const maxThreads = 2 const maxThreads = 2
const maxTracks = 4 const maxSoundTracks = 4
const maxCharacters = 22 const maxCharacters = 22
// Get search page. // Get search page.
@ -20,6 +22,64 @@ func Get(ctx *aero.Context) string {
term := ctx.Get("term") term := ctx.Get("term")
term = strings.TrimPrefix(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)) 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))
}

View File

@ -7,35 +7,40 @@ component SearchResults(term string, users []*arn.User, animes []*arn.Anime, pos
Icon("tv") Icon("tv")
span Anime span Anime
AnimeSearchResults(animes) #anime-search-results
AnimeSearchResults(animes)
.widget .widget
h3.widget-title h3.widget-title
Icon("user") Icon("user")
span Characters span Characters
CharacterSearchResults(characters) #character-search-results
CharacterSearchResults(characters)
.widget .widget
h3.widget-title h3.widget-title
Icon("comment") Icon("comment")
span Forum span Forum
ForumSearchResults(posts, threads) #forum-search-results
ForumSearchResults(posts, threads)
.widget .widget
h3.widget-title h3.widget-title
Icon("music") Icon("music")
span Soundtracks span Soundtracks
SoundTrackSearchResults(tracks) #soundtrack-search-results
SoundTrackSearchResults(tracks)
.widget .widget
h3.widget-title h3.widget-title
Icon("user") Icon("user")
span Users span Users
UserSearchResults(users) #user-search-results
UserSearchResults(users)
component AnimeSearchResults(animes []*arn.Anime) component AnimeSearchResults(animes []*arn.Anime)
if len(animes) == 0 if len(animes) == 0

View File

@ -1,19 +1,126 @@
import { AnimeNotifier } from "../AnimeNotifier" 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 // 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) { if(e.ctrlKey || e.altKey) {
return return
} }
let term = search.value let term = search.value.trim()
let searchPageActivated = (searchPage === arn.app.content.children[0])
if(!term || term.length < 1) { // Reset
arn.app.content.innerHTML = "Please enter at least 1 character to start searching." correctResponseRendered.anime = false
return 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 // Search database
@ -67,7 +174,7 @@ export function searchDB(arn: AnimeNotifier, input: HTMLInputElement, e: Keyboar
} else { } else {
link.href = "/" + dataType.toLowerCase() + "/" + record.id link.href = "/" + dataType.toLowerCase() + "/" + record.id
} }
link.target = "_blank" link.target = "_blank"
container.appendChild(link) container.appendChild(link)

View File

@ -710,6 +710,10 @@ export class AnimeNotifier {
} }
} }
innerHTML(element: HTMLElement, html: string) {
return Diff.innerHTML(element, html)
}
post(url: string, body: any) { post(url: string, body: any) {
if(this.isLoading) { if(this.isLoading) {
return Promise.resolve(null) return Promise.resolve(null)