New user profile
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -6,7 +6,6 @@
|
||||
align-items center
|
||||
margin 0.5rem
|
||||
default-transition
|
||||
transform scale(1)
|
||||
|
||||
:hover
|
||||
.image-title
|
||||
|
@ -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")
|
||||
|
@ -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
37
pages/profile/old.pixy
Normal 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
|
@ -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()))
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user