HTMX lazy load for user statistics card
This commit is contained in:
parent
e7f7f4d0d3
commit
f4721d65da
|
@ -79,14 +79,9 @@ func Profile() http.HandlerFunc {
|
||||||
|
|
||||||
var isSelf = currentUser.ID == user.ID
|
var isSelf = currentUser.ID == user.ID
|
||||||
|
|
||||||
// Banned or disabled? Only admin can view then.
|
// Give a Not Found page if we can not see this user.
|
||||||
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
|
if err := user.CanBeSeenBy(currentUser); err != nil {
|
||||||
templates.NotFoundPage(w, r)
|
log.Error("%s can not be seen by viewer %s: %s", user.Username, currentUser.Username, err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is either one blocking?
|
|
||||||
if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
|
|
||||||
templates.NotFoundPage(w, r)
|
templates.NotFoundPage(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -107,20 +102,14 @@ func Profile() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
vars := map[string]interface{}{
|
vars := map[string]interface{}{
|
||||||
"User": user,
|
"User": user,
|
||||||
"LikeMap": likeMap,
|
"LikeMap": likeMap,
|
||||||
"IsFriend": isFriend,
|
"IsFriend": isFriend,
|
||||||
"IsPrivate": isPrivate,
|
"IsPrivate": isPrivate,
|
||||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||||
"FriendCount": models.CountFriends(user.ID),
|
"FriendCount": models.CountFriends(user.ID),
|
||||||
"ForumThreadCount": models.CountThreadsByUser(user),
|
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
||||||
"ForumReplyCount": models.CountCommentsByUser(user, "threads"),
|
|
||||||
"PhotoCommentCount": models.CountCommentsByUser(user, "photos"),
|
|
||||||
"CommentsReceivedCount": models.CountCommentsReceived(user),
|
|
||||||
"LikesGivenCount": models.CountLikesGiven(user),
|
|
||||||
"LikesReceivedCount": models.CountLikesReceived(user),
|
|
||||||
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
|
||||||
|
|
||||||
// Details on who likes their profile page.
|
// Details on who likes their profile page.
|
||||||
"LikeExample": likeExample,
|
"LikeExample": likeExample,
|
||||||
|
|
80
pkg/controller/htmx/profile_activity.go
Normal file
80
pkg/controller/htmx/profile_activity.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package htmx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Statistics and social activity on the user's profile page.
|
||||||
|
func UserProfileActivityCard() http.HandlerFunc {
|
||||||
|
tmpl := templates.MustLoadCustom("partials/htmx/profile_activity.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
username = r.FormValue("username")
|
||||||
|
)
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
templates.NotFoundPage(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: use ?delay=true to force a slower response.
|
||||||
|
if r.FormValue("delay") != "" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find this user.
|
||||||
|
user, err := models.FindUser(username)
|
||||||
|
if err != nil {
|
||||||
|
templates.NotFoundPage(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user.
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "You must be signed in to view this page.")
|
||||||
|
templates.Redirect(w, "/login?next=/u/"+url.QueryEscape(r.URL.String()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the site under a Maintenance Mode restriction?
|
||||||
|
if middleware.MaintenanceMode(currentUser, w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject relationship booleans for profile picture display.
|
||||||
|
models.SetUserRelationships(currentUser, []*models.User{user})
|
||||||
|
|
||||||
|
// Give a Not Found page if we can not see this user.
|
||||||
|
if err := user.CanBeSeenBy(currentUser); err != nil {
|
||||||
|
log.Error("%s can not be seen by viewer %s: %s", user.Username, currentUser.Username, err)
|
||||||
|
templates.NotFoundPage(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
"User": user,
|
||||||
|
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||||
|
"FriendCount": models.CountFriends(user.ID),
|
||||||
|
"ForumThreadCount": models.CountThreadsByUser(user),
|
||||||
|
"ForumReplyCount": models.CountCommentsByUser(user, "threads"),
|
||||||
|
"PhotoCommentCount": models.CountCommentsByUser(user, "photos"),
|
||||||
|
"CommentsReceivedCount": models.CountCommentsReceived(user),
|
||||||
|
"LikesGivenCount": models.CountLikesGiven(user),
|
||||||
|
"LikesReceivedCount": models.CountLikesReceived(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -226,6 +226,31 @@ func (u *User) IsShyFrom(other *User) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanBeSeenBy checks whether the user can be seen to exist by the viewer.
|
||||||
|
//
|
||||||
|
// An admin viewer can always see them, but a user may be hidden to others when they are
|
||||||
|
// blocking, disabled or banned.
|
||||||
|
//
|
||||||
|
// The user should always be given a Not Found page so they can't tell the user even
|
||||||
|
// exists. The returned error will include a specific reason, for debugging purposes.
|
||||||
|
func (u *User) CanBeSeenBy(viewer *User) error {
|
||||||
|
if viewer.IsAdmin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Banned or disabled? Only admin can view then.
|
||||||
|
if u.Status != UserStatusActive {
|
||||||
|
return fmt.Errorf("user status is %s", u.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is either one blocking?
|
||||||
|
if IsBlocking(viewer.ID, u.ID) && !viewer.IsAdmin {
|
||||||
|
return fmt.Errorf("users block each other")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UserSearch config.
|
// UserSearch config.
|
||||||
type UserSearch struct {
|
type UserSearch struct {
|
||||||
Username string
|
Username string
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/comment"
|
"code.nonshy.com/nonshy/website/pkg/controller/comment"
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/forum"
|
"code.nonshy.com/nonshy/website/pkg/controller/forum"
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/friend"
|
"code.nonshy.com/nonshy/website/pkg/controller/friend"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/controller/htmx"
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/inbox"
|
"code.nonshy.com/nonshy/website/pkg/controller/inbox"
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/index"
|
"code.nonshy.com/nonshy/website/pkg/controller/index"
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/photo"
|
"code.nonshy.com/nonshy/website/pkg/controller/photo"
|
||||||
|
@ -116,6 +117,9 @@ func New() http.Handler {
|
||||||
mux.Handle("POST /v1/barertc/report", barertc.Report())
|
mux.Handle("POST /v1/barertc/report", barertc.Report())
|
||||||
mux.Handle("POST /v1/barertc/profile", barertc.Profile())
|
mux.Handle("POST /v1/barertc/profile", barertc.Profile())
|
||||||
|
|
||||||
|
// HTMX endpoints.
|
||||||
|
mux.Handle("GET /htmx/user/profile/activity", middleware.LoginRequired(htmx.UserProfileActivityCard()))
|
||||||
|
|
||||||
// Redirect endpoints.
|
// Redirect endpoints.
|
||||||
mux.Handle("GET /go/comment", middleware.LoginRequired(comment.GoToComment()))
|
mux.Handle("GET /go/comment", middleware.LoginRequired(comment.GoToComment()))
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,28 @@ func LoadTemplate(filename string) (*Template, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadCustom loads a bare template without the site theme and partial templates attached.
|
||||||
|
//
|
||||||
|
// The custom TempleFuncs and vars are still available (PrettyTitle, .CurrentUser, etc.)
|
||||||
|
func LoadCustom(filename string) (*Template, error) {
|
||||||
|
filepath := config.TemplatePath + "/" + filename
|
||||||
|
stat, err := os.Stat(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := template.New("page")
|
||||||
|
tmpl.Funcs(TemplateFuncs(nil))
|
||||||
|
tmpl.ParseFiles(filepath)
|
||||||
|
|
||||||
|
return &Template{
|
||||||
|
filename: filename,
|
||||||
|
filepath: filepath,
|
||||||
|
modified: stat.ModTime(),
|
||||||
|
tmpl: tmpl,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Must LoadTemplate or panic.
|
// Must LoadTemplate or panic.
|
||||||
func Must(filename string) *Template {
|
func Must(filename string) *Template {
|
||||||
tmpl, err := LoadTemplate(filename)
|
tmpl, err := LoadTemplate(filename)
|
||||||
|
@ -55,6 +77,15 @@ func Must(filename string) *Template {
|
||||||
return tmpl
|
return tmpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must LoadCustom or panic.
|
||||||
|
func MustLoadCustom(filename string) *Template {
|
||||||
|
tmpl, err := LoadCustom(filename)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
// Execute a loaded template. In debug mode, the template file may be reloaded
|
// Execute a loaded template. In debug mode, the template file may be reloaded
|
||||||
// from disk if the file on disk has been modified.
|
// from disk if the file on disk has been modified.
|
||||||
func (t *Template) Execute(w http.ResponseWriter, r *http.Request, vars map[string]interface{}) error {
|
func (t *Template) Execute(w http.ResponseWriter, r *http.Request, vars map[string]interface{}) error {
|
||||||
|
|
1
web/static/js/htmx-1.9.12.min.js
vendored
Normal file
1
web/static/js/htmx-1.9.12.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -451,88 +451,12 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<strong class="has-text-info">Statistics</strong>
|
|
||||||
<table class="table is-fullwidth" style="font-size: small">
|
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
<strong class="is-size-7">Photos shared:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/u/{{.User.Username}}/photos" class="has-text-info">
|
|
||||||
<i class="fa fa-image mr-1"></i>
|
|
||||||
{{FormatNumberCommas .PhotoCount}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong class="is-size-7">Forum threads written:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/forum/search?username={{.User.Username}}&type=threads" class="has-text-info">
|
|
||||||
<i class="fa fa-comment mr-1"></i>
|
|
||||||
{{FormatNumberCommas .ForumThreadCount}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong class="is-size-7">Forum comments:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/forum/search?username={{.User.Username}}" class="has-text-info">
|
|
||||||
<i class="fa fa-comments mr-1"></i>
|
|
||||||
{{FormatNumberCommas .ForumReplyCount}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<strong class="has-text-info">Social</strong>
|
<!-- Lazy load the statistics card-->
|
||||||
<table class="table is-fullwidth" style="font-size: small">
|
<div hx-get="/htmx/user/profile/activity?username={{.User.Username}}&delay=1" hx-trigger="load">
|
||||||
<tr>
|
<i class="fa fa-spinner fa-spin mr-1"></i> Loading...
|
||||||
<td width="50%">
|
</div>
|
||||||
<strong class="is-size-7">Friends:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/u/{{.User.Username}}/friends" class="has-text-info">
|
|
||||||
<i class="fa fa-user-group mr-1"></i> {{FormatNumberCommas .FriendCount}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong class="is-size-7">Photo comments written:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .PhotoCommentCount}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong class="is-size-7">Photo comments received:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .CommentsReceivedCount}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong class="is-size-7">Likes given:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesGivenCount}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong class="is-size-7">Likes received:</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesReceivedCount}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -383,6 +383,7 @@
|
||||||
<script type="text/javascript" src="/static/js/bulma.js?build={{.BuildHash}}"></script>
|
<script type="text/javascript" src="/static/js/bulma.js?build={{.BuildHash}}"></script>
|
||||||
<script type="text/javascript" src="/static/js/likes.js?build={{.BuildHash}}"></script>
|
<script type="text/javascript" src="/static/js/likes.js?build={{.BuildHash}}"></script>
|
||||||
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/htmx-1.9.12.min.js"></script>
|
||||||
{{template "scripts" .}}
|
{{template "scripts" .}}
|
||||||
|
|
||||||
<!-- Likes modal -->
|
<!-- Likes modal -->
|
||||||
|
|
84
web/templates/partials/htmx/profile_activity.html
Normal file
84
web/templates/partials/htmx/profile_activity.html
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
{{define "base"}}
|
||||||
|
<strong class="has-text-info">Statistics</strong>
|
||||||
|
<table class="table is-fullwidth" style="font-size: small">
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<strong class="is-size-7">Photos shared:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/u/{{.User.Username}}/photos" class="has-text-info">
|
||||||
|
<i class="fa fa-image mr-1"></i>
|
||||||
|
{{FormatNumberCommas .PhotoCount}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong class="is-size-7">Forum threads written:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/forum/search?username={{.User.Username}}&type=threads" class="has-text-info">
|
||||||
|
<i class="fa fa-comment mr-1"></i>
|
||||||
|
{{FormatNumberCommas .ForumThreadCount}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong class="is-size-7">Forum comments:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/forum/search?username={{.User.Username}}" class="has-text-info">
|
||||||
|
<i class="fa fa-comments mr-1"></i>
|
||||||
|
{{FormatNumberCommas .ForumReplyCount}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<strong class="has-text-info">Social</strong>
|
||||||
|
<table class="table is-fullwidth" style="font-size: small">
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<strong class="is-size-7">Friends:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/u/{{.User.Username}}/friends" class="has-text-info">
|
||||||
|
<i class="fa fa-user-group mr-1"></i> {{FormatNumberCommas .FriendCount}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong class="is-size-7">Photo comments written:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .PhotoCommentCount}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong class="is-size-7">Photo comments received:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .CommentsReceivedCount}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong class="is-size-7">Likes given:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesGivenCount}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong class="is-size-7">Likes received:</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesReceivedCount}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
Loading…
Reference in New Issue
Block a user