diff --git a/main.go b/main.go index b6dfb3d0..39aff0a8 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,7 @@ func main() { app.Ajax("/user/:nick/animelist/:id", animelistitem.Get) app.Ajax("/settings", settings.Get) app.Ajax("/admin", admin.Get) + app.Ajax("/search/:term", search.Get) app.Ajax("/users", users.Get) app.Ajax("/airing", airing.Get) app.Ajax("/webdev", webdev.Get) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 4eeb6668..5767e511 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -32,7 +32,7 @@ component LoggedInMenu(user *arn.User) NavigationButtonNoAJAX("Logout", "/logout", "sign-out") component FuzzySearch - input#search(type="text", placeholder="Search...", title="Shortcut: Ctrl + Q") + input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: Ctrl + Q") component NavigationButton(name string, target string, icon string) a.navigation-link.ajax(href=target, aria-label=name) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index bfaa6370..d97034f1 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -26,7 +26,7 @@ component Anime(anime *arn.Anime, user *arn.User) Icon("pencil") span Edit in collection else - button.action(data-action="addAnimeToCollection", data-anime-id=anime.ID, data-user-id=user.ID, data-user-nick=user.Nick) + button.action(data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID, data-user-id=user.ID, data-user-nick=user.Nick) Icon("plus") span Add to collection diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index f15815a6..e7721a6e 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -17,6 +17,6 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. a.ajax.button(href=anime.Link()) Icon("search-plus") span View anime - button.action(data-action="removeAnimeFromCollection", data-anime-id=anime.ID, data-user-id=viewUser.ID, data-user-nick=viewUser.Nick) + button.action(data-action="removeAnimeFromCollection", data-trigger="click", data-anime-id=anime.ID, data-user-id=viewUser.ID, data-user-nick=viewUser.Nick) Icon("trash") span Remove from collection \ No newline at end of file diff --git a/pages/popular-anime/popular.go b/pages/popular-anime/popular.go new file mode 100644 index 00000000..5d141f59 --- /dev/null +++ b/pages/popular-anime/popular.go @@ -0,0 +1,29 @@ +package popularanime + +import ( + "github.com/aerogo/aero" +) + +// Get search page. +func Get(ctx *aero.Context) string { + // titleCount := 0 + // animeCount := 0 + + // // let info: any = await bluebird.props({ + // // popular: arn.db.get('Cache', 'popularAnime'), + // // stats: arn.db.get('Cache', 'animeStats') + // // }) + + // // return response.render({ + // // user, + // // popularAnime: info.popular.anime, + // // animeCount: info.stats.animeCount, + // // titleCount: info.stats.titleCount, + // // anime: null + // // }) + + // popular, _ := arn.GetPopularCache() + + // return ctx.HTML(components.Search(popular.Anime, titleCount, animeCount)) + return ctx.HTML("Coming soon.") +} diff --git a/pages/popular-anime/popular.pixy b/pages/popular-anime/popular.pixy new file mode 100644 index 00000000..dc922971 --- /dev/null +++ b/pages/popular-anime/popular.pixy @@ -0,0 +1,16 @@ +component Search(popularAnime []*arn.Anime, titleCount int, animeCount int) + h2 Anime + + #search-container + input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount) + + #search-results-container + #search-results + + if popularAnime != nil + h3.popular-title Popular + + .popular-anime-list + each anime in popularAnime + a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)") + img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji) \ No newline at end of file diff --git a/pages/search/popular.scarlet b/pages/popular-anime/popular.scarlet similarity index 100% rename from pages/search/popular.scarlet rename to pages/popular-anime/popular.scarlet diff --git a/pages/search/search.go b/pages/search/search.go index 8091fc4c..ff0f10e4 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -1,29 +1,97 @@ package search import ( + "strings" + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" ) +const maxUsers = 18 +const maxAnime = 18 + +type AnimeID = string +type UserID = string + +var animeSearchIndex = make(map[string]AnimeID) +var userSearchIndex = make(map[string]UserID) + +func init() { + updateSearchIndex() +} + +func updateSearchIndex() { + updateAnimeIndex() + updateUserIndex() +} + +func updateAnimeIndex() { + // Anime + animeStream, err := arn.AllAnime() + + if err != nil { + panic(err) + } + + for anime := range animeStream { + animeSearchIndex[strings.ToLower(anime.Title.Canonical)] = anime.ID + } +} + +func updateUserIndex() { + // Users + userStream, err := arn.AllUsers() + + if err != nil { + panic(err) + } + + for user := range userStream { + userSearchIndex[strings.ToLower(user.Nick)] = user.ID + } +} + // Get search page. func Get(ctx *aero.Context) string { - // titleCount := 0 - // animeCount := 0 + term := strings.ToLower(ctx.Get("term")) - // // let info: any = await bluebird.props({ - // // popular: arn.db.get('Cache', 'popularAnime'), - // // stats: arn.db.get('Cache', 'animeStats') - // // }) + var users []*arn.User + var animeResults []*arn.Anime - // // return response.render({ - // // user, - // // popularAnime: info.popular.anime, - // // animeCount: info.stats.animeCount, - // // titleCount: info.stats.titleCount, - // // anime: null - // // }) + aero.Parallel(func() { + for name, id := range userSearchIndex { + if strings.Index(name, term) != -1 { + user, err := arn.GetUser(id) - // popular, _ := arn.GetPopularCache() + if err != nil { + continue + } - // return ctx.HTML(components.Search(popular.Anime, titleCount, animeCount)) - return ctx.HTML("Coming soon.") + users = append(users, user) + + if len(users) >= maxUsers { + break + } + } + } + }, func() { + for title, id := range animeSearchIndex { + if strings.Index(title, term) != -1 { + anime, err := arn.GetAnime(id) + + if err != nil { + continue + } + + animeResults = append(animeResults, anime) + + if len(animeResults) >= maxAnime { + break + } + } + } + }) + + return ctx.HTML(components.Search(users, animeResults)) } diff --git a/pages/search/search.pixy b/pages/search/search.pixy index dc922971..312ded75 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -1,16 +1,21 @@ -component Search(popularAnime []*arn.Anime, titleCount int, animeCount int) - h2 Anime - - #search-container - input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount) - - #search-results-container - #search-results - - if popularAnime != nil - h3.popular-title Popular - - .popular-anime-list - each anime in popularAnime - a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)") - img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji) \ No newline at end of file +component Search(users []*arn.User, animeResults []*arn.Anime) + .widgets + .widget + h3 Users + .user-avatars.user-search + if len(users) == 0 + p No users found. + else + each user in users + Avatar(user) + //- a.ajax(href=user.Link())= user.Nick + + .widget + h3 Anime + .profile-watching-list.anime-search + if len(animeResults) == 0 + p No anime found. + else + each anime in animeResults + a.profile-watching-list-item.ajax(href=anime.Link(), title=anime.Title.Canonical) + img.anime-cover-image.anime-search-result(src=anime.Image.Tiny, alt=anime.Title.Canonical) \ No newline at end of file diff --git a/pages/search/search.scarlet b/pages/search/search.scarlet index 6020f6cb..9253f4fe 100644 --- a/pages/search/search.scarlet +++ b/pages/search/search.scarlet @@ -1,7 +1,2 @@ -// #search-container -// horizontal -// justify-content center - -// #search -// width 100% -// max-width 560px \ No newline at end of file +.anime-search-result + width 55px !important \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 18fca0fd..661a79b7 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -38,9 +38,11 @@ export class AnimeNotifier { for(let element of findAll(".action")) { let actionName = element.dataset.action - element.onclick = () => { - actions[actionName](this, element) - } + element.addEventListener(element.dataset.trigger, e => { + actions[actionName](this, element, e) + }) + + element.classList.remove("action") } } diff --git a/scripts/Application.ts b/scripts/Application.ts index 308db6d1..7489d0f6 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -32,6 +32,11 @@ export class Application { } get(url: string): Promise { + if(this.lastRequest) { + this.lastRequest.abort() + this.lastRequest = null + } + return new Promise((resolve, reject) => { let request = new XMLHttpRequest() @@ -52,11 +57,6 @@ export class Application { } load(url: string, options?: LoadOptions) { - if(this.lastRequest) { - this.lastRequest.abort() - this.lastRequest = null - } - if(!options) { options = new LoadOptions() } diff --git a/scripts/actions.ts b/scripts/actions.ts index dff331b3..4648ee50 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -2,6 +2,38 @@ import { Application } from "./Application" import { AnimeNotifier } from "./AnimeNotifier" import { Diff } from "./Diff" +// Search +export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { + if(e.ctrlKey || e.altKey) { + return + } + + let term = search.value + + if(!window.location.pathname.startsWith("/search/")) { + history.pushState("search", null, "/search/" + term) + } else { + history.replaceState("search", null, "/search/" + term) + } + + if(!term) { + arn.app.content.innerHTML = "No search term." + return + } + + arn.app.content.innerHTML = "

" + term + "

" + + arn.app.get("/_/search/" + encodeURI(term)) + .then(html => { + if(!search.value) { + return + } + + arn.app.find("results").innerHTML = html + arn.app.emit("DOMContentLoaded") + }) +} + // Add anime to collection export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { button.innerText = "Adding..."