Skip to content

Commit

Permalink
add: infinite scroll for users
Browse files Browse the repository at this point in the history
  • Loading branch information
thelissimus-work committed Mar 1, 2025
1 parent fc6b36e commit 6b8b659
Show file tree
Hide file tree
Showing 3 changed files with 444 additions and 330 deletions.
15 changes: 12 additions & 3 deletions modules/core/presentation/controllers/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (c *UsersController) Register(r *mux.Router) {

func (c *UsersController) Users(w http.ResponseWriter, r *http.Request) {
params := composables.UsePaginated(r)
us, err := c.userService.GetPaginated(r.Context(), &user.FindParams{
us, total, err := c.userService.GetPaginatedWithTotal(r.Context(), &user.FindParams{
Limit: params.Limit,
Offset: params.Offset,
SortBy: user.SortBy{Fields: []user.Field{}},
Expand All @@ -76,10 +76,19 @@ func (c *UsersController) Users(w http.ResponseWriter, r *http.Request) {
}
isHxRequest := len(r.Header.Get("Hx-Request")) > 0
props := &users.IndexPageProps{
Users: mapping.MapViewModels(us, mappers.UserToViewModel),
Users: mapping.MapViewModels(us, mappers.UserToViewModel),
Page: params.Page,
PerPage: params.Limit,
Search: r.URL.Query().Get("name"),
HasMore: total > int64(params.Page*params.Limit),
}
if isHxRequest {
templ.Handler(users.UsersTable(props), templ.WithStreaming()).ServeHTTP(w, r)
if params.Page > 1 {
// pages after the first one are served as extensions of the previous table due to infinite scroll
templ.Handler(users.UserRows(props), templ.WithStreaming()).ServeHTTP(w, r)
} else {
templ.Handler(users.UsersTable(props), templ.WithStreaming()).ServeHTTP(w, r)
}
} else {
templ.Handler(users.Index(props), templ.WithStreaming()).ServeHTTP(w, r)
}
Expand Down
138 changes: 83 additions & 55 deletions modules/core/presentation/templates/pages/users/users.templ
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,83 @@ import (
)

type IndexPageProps struct {
Users []*viewmodels.User
Users []*viewmodels.User
Page int
PerPage int
Search string
HasMore bool
}

func mkInfiniteAttrs(props *IndexPageProps) templ.Attributes {
return templ.Attributes{
"hx-get": fmt.Sprintf("/users?page=%d&name=%s&limit=%d", props.Page+1, props.Search, props.PerPage),
"hx-trigger": "revealed",
"hx-swap": "afterend",
"hx-target": "this",
}
}

templ UserRow(props *IndexPageProps, user *viewmodels.User, isLastRow bool) {
{{ pageCtx := composables.UsePageCtx(ctx) }}
{{ rowProps := &base.TableRowProps{} }}
if isLastRow && props.HasMore {
{{ rowProps.Attrs = mkInfiniteAttrs(props) }}
}
@base.TableRow(rowProps) {
@base.TableCell() {
<div class="flex items-center gap-3">
if user.Avatar != nil && user.Avatar.URL != "" {
<img
src={ user.Avatar.URL }
class="w-8 h-8 object-cover rounded-full"
alt="User Avatar"
/>
} else {
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-brand-500/10 text-brand-500 font-medium text-sm">
{ string(user.FirstName[0]) }
</div>
}
<span>
{ user.FullName() }
</span>
</div>
}
@base.TableCell() {
if user.LastAction != "" {
<div>
{ user.RolesVerbose() }
</div>
} else {
{ pageCtx.T("Unknown") }
}
}
@base.TableCell() {
<div>
{ user.Email }
</div>
}
@base.TableCell() {
<div x-data="relativeformat">
<span x-text={ fmt.Sprintf("format('%s')", user.UpdatedAt) }></span>
</div>
}
@base.TableCell() {
@button.Secondary(button.Props{
Fixed: true,
Size: button.SizeSM,
Class: "btn-fixed",
Href: fmt.Sprintf("/users/%s", user.ID),
}) {
@icons.PencilSimple(icons.Props{Size: "20"})
}
}
}
}

templ UserRows(props *IndexPageProps) {
for ix, user := range props.Users {
@UserRow(props, user, ix == len(props.Users)-1)
}
}

templ UsersTable(props *IndexPageProps) {
Expand All @@ -27,57 +103,7 @@ templ UsersTable(props *IndexPageProps) {
{Label: pageCtx.T("Actions"), Class: "w-16"},
},
}) {
for _, user := range props.Users {
@base.TableRow(&base.TableRowProps{}) {
@base.TableCell() {
<div class="flex items-center gap-3">
if user.Avatar != nil && user.Avatar.URL != "" {
<img
src={ user.Avatar.URL }
class="w-8 h-8 object-cover rounded-full"
alt="User Avatar"
/>
} else {
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-brand-500/10 text-brand-500 font-medium text-sm">
{ string(user.FirstName[0]) }
</div>
}
<span>
{ user.FullName() }
</span>
</div>
}
@base.TableCell() {
if user.LastAction != "" {
<div>
{ user.RolesVerbose() }
</div>
} else {
{ pageCtx.T("Unknown") }
}
}
@base.TableCell() {
<div>
{ user.Email }
</div>
}
@base.TableCell() {
<div x-data="relativeformat">
<span x-text={ fmt.Sprintf("format('%s')", user.UpdatedAt) }></span>
</div>
}
@base.TableCell() {
@button.Secondary(button.Props{
Fixed: true,
Size: button.SizeSM,
Class: "btn-fixed",
Href: fmt.Sprintf("/users/%s", user.ID),
}) {
@icons.PencilSimple(icons.Props{Size: "20"})
}
}
}
}
@UserRows(props)
}
}

Expand All @@ -93,7 +119,7 @@ templ UsersContent(props *IndexPageProps) {
"name": "limit",
},
}) {
<option>15</option>
<option selected>15</option>
<option>25</option>
<option>50</option>
<option>100</option>
Expand Down Expand Up @@ -133,14 +159,16 @@ templ UsersContent(props *IndexPageProps) {
hx-target="table"
hx-swap="outerHTML"
>
<input type="hidden" name="page" value="1"/>
<div class="flex-1">
@input.Text(&input.Props{
AddonLeft: &input.Addon{
Component: icons.MagnifyingGlass(icons.Props{Size: "20"}),
},
Placeholder: pageCtx.T("Search"),
Attrs: templ.Attributes{
"name": "name",
"name": "name",
"value": props.Search,
},
})
</div>
Expand All @@ -151,7 +179,7 @@ templ UsersContent(props *IndexPageProps) {
"name": "limit",
},
}) {
<option>15</option>
<option selected>15</option>
<option>25</option>
<option>50</option>
<option>100</option>
Expand Down
Loading

0 comments on commit 6b8b659

Please sign in to comment.