New user profile

This commit is contained in:
2018-11-15 20:19:40 +09:00
parent a1c11a2eae
commit d5dff615c1
29 changed files with 271 additions and 141 deletions

View File

@ -37,7 +37,7 @@ func fetchActivities(user *arn.User, followedOnly bool) []arn.Activity {
return false
}
if activity.Type() == "ActivityCreate" {
if activity.TypeName() == "ActivityCreate" {
obj := activity.(*arn.ActivityCreate).Object()
if obj == nil {
@ -48,7 +48,7 @@ func fetchActivities(user *arn.User, followedOnly bool) []arn.Activity {
return !isDraftable || !draft.GetIsDraft()
}
if activity.Type() == "ActivityConsumeAnime" {
if activity.TypeName() == "ActivityConsumeAnime" {
return activity.(*arn.ActivityConsumeAnime).Anime() != nil
}

View File

@ -32,20 +32,20 @@ component Activity(activity arn.Activity, user *arn.User)
.post-content
.activity-header
.activity-parent
if activity.Type() == "ActivityCreate"
if activity.TypeName() == "ActivityCreate"
ActivityCreateTitle(activity.(*arn.ActivityCreate), user)
else if activity.Type() == "ActivityConsumeAnime"
else if activity.TypeName() == "ActivityConsumeAnime"
ActivityConsumeAnimeTitle(activity.(*arn.ActivityConsumeAnime), user)
if user != nil && user.ID == activity.GetCreatedBy() && activity.Type() == "ActivityConsumeAnime"
button.activity-action.tip.action(data-action="deleteObject", data-trigger="click", aria-label="Delete", data-return-path="/activity", data-confirm-type="activity", data-api=fmt.Sprintf("/api/%s/%s", strings.ToLower(activity.Type()), activity.GetID()))
if user != nil && user.ID == activity.GetCreatedBy() && activity.TypeName() == "ActivityConsumeAnime"
button.activity-action.tip.action(data-action="deleteObject", data-trigger="click", aria-label="Delete", data-return-path="/activity", data-confirm-type="activity", data-api=fmt.Sprintf("/api/%s/%s", strings.ToLower(activity.TypeName()), activity.GetID()))
RawIcon("trash")
.activity-date.utc-date(data-date=activity.GetCreated())
if activity.Type() == "ActivityCreate"
if activity.TypeName() == "ActivityCreate"
ActivityCreateText(activity.(*arn.ActivityCreate), user)
else if activity.Type() == "ActivityConsumeAnime"
else if activity.TypeName() == "ActivityConsumeAnime"
ActivityConsumeAnimeText(activity.(*arn.ActivityConsumeAnime), user)
component ActivityConsumeAnimeTitle(activity *arn.ActivityConsumeAnime, user *arn.User)

View File

@ -6,7 +6,6 @@
align-items center
margin 0.5rem
default-transition
transform scale(1)
:hover
.image-title

View File

@ -33,5 +33,5 @@ component CompareMAL(comparisons []*utils.MALComparison, year string, status str
.data-comparison-difference-detail= difference.DetailsA()
.data-comparison-difference-detail= difference.DetailsB()
button.data-comparison-difference-ignore.action(data-action="newAnimeDiffIgnore", data-trigger="click", data-id=arn.CreateDifferenceID(comparison.Anime.ID, "mal", comparison.MALAnime.ID, difference.Type()), data-hash=difference.Hash())
button.data-comparison-difference-ignore.action(data-action="newAnimeDiffIgnore", data-trigger="click", data-id=arn.CreateDifferenceID(comparison.Anime.ID, "mal", comparison.MALAnime.ID, difference.TypeName()), data-hash=difference.Hash())
RawIcon("trash")

View File

@ -19,5 +19,5 @@ func ReplyUI(ctx *aero.Context) string {
return ctx.Error(http.StatusNotFound, "Post not found", err)
}
return ctx.HTML(components.NewPostArea(user, "Reply") + components.NewPostActions(post.Type(), post.ID, true))
return ctx.HTML(components.NewPostArea(post, user, "Reply") + components.NewPostActions(post, true))
}

37
pages/profile/old.pixy Normal file
View File

@ -0,0 +1,37 @@
//- component ProfileTabs(viewUser *arn.User, uri string)
//- .tabs.mountable.never-unmount
//- Tab("Anime", "th", "/+" + viewUser.Nick)
//- Tab("Characters", "child", "/+" + viewUser.Nick + "/characters/liked")
//- Tab("Forum", "comment", "/+" + viewUser.Nick + "/forum/threads")
//- Tab("Tracks", "music", "/+" + viewUser.Nick + "/soundtracks/liked")
//- Tab("Quotes", "quote-left", "/+" + viewUser.Nick + "/quotes/liked")
//- Tab("Stats", "area-chart", "/+" + viewUser.Nick + "/stats")
//- Tab("Followers", "users", "/+" + viewUser.Nick + "/followers")
//- if strings.Contains(uri, "/soundtracks")
//- .tabs
//- Tab("Liked", "heart", "/+" + viewUser.Nick + "/soundtracks/liked")
//- Tab("Added", "plus", "/+" + viewUser.Nick + "/soundtracks/added")
//- if strings.Contains(uri, "/quotes")
//- .tabs
//- Tab("Liked", "heart", "/+" + viewUser.Nick + "/quotes/liked")
//- Tab("Added", "plus", "/+" + viewUser.Nick + "/quotes/added")
//- Anime shelf
//- if len(animeList.Items) == 0
//- p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet."
//- else
//- .profile-watching-list.mountable
//- each item in animeList.Items
//- if item.Status == arn.AnimeListStatusWatching || item.Status == arn.AnimeListStatusCompleted
//- a.profile-watching-list-item.tip(href=item.Anime().Link(), aria-label=item.Anime().Title.ByUser(user) + " (" + fmt.Sprint(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")")
//- img.profile-watching-list-item-image.lazy(data-src=item.Anime().ImageLink("small"), data-webp="true", data-color=item.Anime().AverageColor(), alt=item.Anime().Title.ByUser(user), importance="high")
//- Footer
//- .footer
//- .buttons
//- if user != nil && (user.Role == "admin" || user.Role == "editor")
//- a.button.profile-action(href="/api/user/" + viewUser.ID, target="_blank", rel="noopener")
//- Icon("search-plus")
//- span JSON

View File

@ -1,12 +1,19 @@
package profile
import (
"sort"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
const (
maxCharacters = 6
maxFriends = 7
)
// Get user profile page.
func Get(ctx *aero.Context) string {
nick := ctx.Get("nick")
@ -22,6 +29,8 @@ func Get(ctx *aero.Context) string {
// Profile renders the user profile page of the given viewUser.
func Profile(ctx *aero.Context, viewUser *arn.User) string {
user := utils.GetUser(ctx)
// Anime list
animeList := viewUser.AnimeList()
if user == nil || user.ID != viewUser.ID {
@ -30,6 +39,10 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string {
animeList.SortByRating()
// Genres
topGenres := animeList.TopGenres(5)
// Open graph
openGraph := &arn.OpenGraph{
Tags: map[string]string{
"og:title": viewUser.Nick,
@ -46,7 +59,39 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string {
},
}
ctx.Data = openGraph
// Friends
friends := viewUser.Follows().UsersWhoFollowBack()
return ctx.HTML(components.Profile(viewUser, user, animeList, ctx.URI()))
arn.SortUsersFollowers(friends)
if len(friends) > maxFriends {
friends = friends[:maxFriends]
}
// Characters
characters := []*arn.Character{}
for character := range arn.StreamCharacters() {
if arn.Contains(character.Likes, viewUser.ID) {
characters = append(characters, character)
}
}
sort.Slice(characters, func(i, j int) bool {
aLikes := len(characters[i].Likes)
bLikes := len(characters[j].Likes)
if aLikes == bLikes {
return characters[i].Name.Canonical < characters[j].Name.Canonical
}
return aLikes > bLikes
})
if len(characters) > maxCharacters {
characters = characters[:maxCharacters]
}
ctx.Data = openGraph
return ctx.HTML(components.Profile(viewUser, user, animeList, characters, friends, topGenres, ctx.URI()))
}

View File

@ -1,48 +1,53 @@
component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, uri string)
ProfileHeader(viewUser, user, uri)
component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, characters []*arn.Character, friends []*arn.User, topGenres []string, uri string)
.profile
ProfileHeader(viewUser, user, uri)
//- if len(animeList.Items) == 0
//- p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet."
//- else
//- .profile-watching-list.mountable
//- each item in animeList.Items
//- if item.Status == arn.AnimeListStatusWatching || item.Status == arn.AnimeListStatusCompleted
//- a.profile-watching-list-item.tip(href=item.Anime().Link(), aria-label=item.Anime().Title.ByUser(user) + " (" + fmt.Sprint(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")")
//- img.profile-watching-list-item-image.lazy(data-src=item.Anime().ImageLink("small"), data-webp="true", data-color=item.Anime().AverageColor(), alt=item.Anime().Title.ByUser(user), importance="high")
.profile-columns
.profile-column.profile-favorites
.profile-section
h3.profile-column-header.mountable Anime
.profile-favorite-anime-container
each item in animeList.Top(6)
a.profile-favorite-anime.tip.mountable(href=item.Anime().Link(), aria-label=item.Anime().Title.ByUser(user), data-mountable-type="favorites")
img.profile-favorite-anime-image.lazy(data-src=item.Anime().ImageLink("small"), data-webp=true, alt=item.Anime().Title.ByUser(user))
//- .footer
//- .buttons
//- if user != nil && (user.Role == "admin" || user.Role == "editor")
//- a.button.profile-action(href="/api/user/" + viewUser.ID, target="_blank", rel="noopener")
//- Icon("search-plus")
//- span JSON
.profile-section
h3.profile-column-header.mountable(data-mountable-type="favorites") Characters
.profile-favorite-characters-container
each character in characters
.mountable(data-mountable-type="favorites")
CharacterSmall(character, user)
//- a.profile-favorite-character.tip.mountable(href=character.Link(), aria-label=character.Name.ByUser(user), data-mountable-type="favorite-anime")
//- img.profile-favorite-character-image.lazy(data-src=character.ImageLink("small"), data-webp=true, alt=character.Name.ByUser(user))
component ProfileTabs(viewUser *arn.User, uri string)
.tabs.mountable.never-unmount
Tab("Anime", "th", "/+" + viewUser.Nick)
Tab("Characters", "child", "/+" + viewUser.Nick + "/characters/liked")
Tab("Forum", "comment", "/+" + viewUser.Nick + "/forum/threads")
Tab("Tracks", "music", "/+" + viewUser.Nick + "/soundtracks/liked")
Tab("Quotes", "quote-left", "/+" + viewUser.Nick + "/quotes/liked")
Tab("Stats", "area-chart", "/+" + viewUser.Nick + "/stats")
Tab("Followers", "users", "/+" + viewUser.Nick + "/followers")
if strings.Contains(uri, "/soundtracks")
.tabs
Tab("Liked", "heart", "/+" + viewUser.Nick + "/soundtracks/liked")
Tab("Added", "plus", "/+" + viewUser.Nick + "/soundtracks/added")
.profile-column.profile-activity
.profile-section
h3.profile-column-header.mountable(data-mountable-type="activity") Activity
Comments(viewUser, user)
.profile-column.profile-extra
.profile-section
h3.profile-column-header.mountable(data-mountable-type="extra") Genres
if strings.Contains(uri, "/quotes")
.tabs
Tab("Liked", "heart", "/+" + viewUser.Nick + "/quotes/liked")
Tab("Added", "plus", "/+" + viewUser.Nick + "/quotes/added")
.anime-genres
each genre in topGenres
a.anime-genre.mountable(href="/genre/" + strings.ToLower(genre), data-mountable-type="extra")= genre
.profile-section
h3.profile-column-header.mountable(data-mountable-type="extra") Friends
.profile-friends
each friend in friends
.profile-friend.mountable(data-mountable-type="extra")
Avatar(friend)
component ProfileHeader(viewUser *arn.User, user *arn.User, uri string)
ProfileHead(viewUser, user, uri)
//- ProfileTabs(viewUser, uri)
component ProfileHead(viewUser *arn.User, user *arn.User, uri string)
.profile
.profile-head
img.profile-cover.lazy(data-src=viewUser.CoverLink("large"), data-webp="true", alt="Cover image")
.profile-image-container.mountable.never-unmount
@ -137,5 +142,4 @@ component ProfileHead(viewUser *arn.User, user *arn.User, uri string)
else
button.profile-action.action.mountable.never-unmount(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove/" + viewUser.ID)
Icon("user-times")
span Unfollow
span Unfollow

View File

@ -1,6 +1,6 @@
const profile-image-size = 280px
.profile
.profile-head
vertical
align-items center
@ -40,8 +40,59 @@ const profile-image-size = 280px
color pro-color
animation sk-pulse 1.5s infinite linear
.profile-columns
vertical
.profile-column
// border 1px solid red
// height 100px
padding calc(content-padding / 2)
.profile-column-header
font-style bold
margin-bottom 1rem
.profile-section
margin-bottom 1rem
.profile-favorite-anime-container
display grid
grid-template-columns repeat(auto-fill, anime-image-small-width)
grid-template-rows repeat(auto-fill, anime-image-small-height)
grid-gap 0.5rem
justify-content space-evenly
.profile-friends
display grid
grid-template-columns repeat(auto-fill, avatar-size)
grid-template-rows repeat(auto-fill, avatar-size)
grid-gap 0.5rem
margin 0 0.5rem
justify-content space-evenly
.profile-favorite-anime
// anime-mini-item
.profile-favorite-anime-image
// anime-mini-item-image
border-radius ui-element-border-radius
.profile-favorite-characters-container
display grid
grid-template-columns repeat(auto-fill, character-image-small-width)
grid-template-rows repeat(auto-fill, character-image-small-height)
grid-gap 0.5rem
justify-content space-evenly
// .profile-favorite-character
// margin 0.25rem
// .profile-favorite-character-image
// border-radius ui-element-border-radius
> 740px
.profile
.profile-head
horizontal
align-items stretch
@ -61,6 +112,10 @@ const profile-image-size = 280px
padding content-padding
margin-top 0
.profile-columns
display grid
grid-template-columns 27% 46% 27%
.profile-pro-status
position absolute
right 0

View File

@ -30,7 +30,7 @@ func Anime(ctx *aero.Context) string {
completed := animeList.FilterStatus(arn.AnimeListStatusCompleted)
// Genre affinity
bestGenres := getBestGenres(animeList)
bestGenres := animeList.TopGenres(bestGenreCount)
// Get all anime
var tv []*arn.Anime

View File

@ -1,43 +0,0 @@
package recommended
import (
"sort"
"github.com/animenotifier/arn"
)
// getBestGenres returns the most liked genres for the user's anime list.
func getBestGenres(animeList *arn.AnimeList) []string {
genreItems := animeList.Genres()
genreAffinity := map[string]float64{}
bestGenres := []string{}
for genre, animeListItems := range genreItems {
affinity := 0.0
for _, item := range animeListItems {
if item.Status != arn.AnimeListStatusCompleted {
continue
}
if item.Rating.Overall != 0 {
affinity += item.Rating.Overall
} else {
affinity += 5.0
}
}
genreAffinity[genre] = affinity
bestGenres = append(bestGenres, genre)
}
sort.Slice(bestGenres, func(i, j int) bool {
return genreAffinity[bestGenres[i]] > genreAffinity[bestGenres[j]]
})
if len(bestGenres) > bestGenreCount {
bestGenres = bestGenres[:bestGenreCount]
}
return bestGenres
}

View File

@ -19,5 +19,5 @@ func ReplyUI(ctx *aero.Context) string {
return ctx.Error(http.StatusNotFound, "Thread not found", err)
}
return ctx.HTML(components.NewPostArea(user, "Reply") + components.NewPostActions(thread.Type(), thread.ID, true))
return ctx.HTML(components.NewPostArea(thread, user, "Reply") + components.NewPostActions(thread, true))
}

View File

@ -11,11 +11,11 @@ component Thread(thread *arn.Thread, user *arn.User)
footer.footer.mountable
p.text-center This topic is locked.
else
NewPostArea(user, "Reply")
NewPostArea(thread, user, "Reply")
.buttons
if !thread.Locked
NewPostActions("Thread", thread.ID, false)
NewPostActions(thread, false)
if user.Role == "admin" || user.Role == "editor"
if thread.Locked