appview/pages: notifications ui and templating #595

merged
opened by anirudh.fi targeting master from push-xwotmtuuvokm
Changed files
+498 -15
appview
pages
templates
errors
layouts
fragments
notifications
strings
user
+40
appview/pages/pages.go
···
return p.execute("user/settings/profile", w, params)
}
+
type NotificationsParams struct {
+
LoggedInUser *oauth.User
+
Notifications []*models.NotificationWithEntity
+
UnreadCount int
+
HasMore bool
+
NextOffset int
+
Limit int
+
}
+
+
func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error {
+
return p.execute("notifications/list", w, params)
+
}
+
+
type NotificationItemParams struct {
+
Notification *models.Notification
+
}
+
+
func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error {
+
return p.executePlain("notifications/fragments/item", w, params)
+
}
+
+
type NotificationCountParams struct {
+
Count int
+
}
+
+
func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error {
+
return p.executePlain("notifications/fragments/count", w, params)
+
}
+
type UserKeysSettingsParams struct {
LoggedInUser *oauth.User
PubKeys []models.PublicKey
···
return p.execute("user/settings/emails", w, params)
}
+
type UserNotificationSettingsParams struct {
+
LoggedInUser *oauth.User
+
Preferences *models.NotificationPreferences
+
Tabs []map[string]any
+
Tab string
+
}
+
+
func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error {
+
return p.execute("user/settings/notifications", w, params)
+
}
+
type UpgradeBannerParams struct {
Registrations []models.Registration
Spindles []models.Spindle
+4 -11
appview/pages/templates/errors/500.html
···
<div class="bg-white dark:bg-gray-800 rounded-lg drop-shadow-sm p-8 max-w-lg mx-auto">
<div class="mb-6">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
-
{{ i "alert-triangle" "w-8 h-8 text-red-500 dark:text-red-400" }}
+
{{ i "triangle-alert" "w-8 h-8 text-red-500 dark:text-red-400" }}
</div>
</div>
···
500 &mdash; internal server error
</h1>
<p class="text-gray-600 dark:text-gray-300">
-
Something went wrong on our end. We've been notified and are working to fix the issue.
-
</p>
-
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded p-3 text-sm text-yellow-800 dark:text-yellow-200">
-
<div class="flex items-center gap-2">
-
{{ i "info" "w-4 h-4" }}
-
<span class="font-medium">we're on it!</span>
-
</div>
-
<p class="mt-1">Our team has been automatically notified about this error.</p>
-
</div>
+
We encountered an error while processing your request. Please try again later.
+
</p>
<div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
<button onclick="location.reload()" class="btn-create gap-2">
{{ i "refresh-cw" "w-4 h-4" }}
try again
</button>
<a href="/" class="btn no-underline hover:no-underline gap-2">
-
{{ i "home" "w-4 h-4" }}
+
{{ i "arrow-left" "w-4 h-4" }}
back to home
</a>
</div>
+2 -1
appview/pages/templates/layouts/fragments/topbar.html
···
<div id="right-items" class="flex items-center gap-2">
{{ with .LoggedInUser }}
{{ block "newButton" . }} {{ end }}
+
{{ template "notifications/fragments/bell" }}
{{ block "dropDown" . }} {{ end }}
{{ else }}
<a href="/login">login</a>
···
{{ define "dropDown" }}
<details class="relative inline-block text-left nav-dropdown">
<summary
-
class="cursor-pointer list-none flex items-center"
+
class="cursor-pointer list-none flex items-center gap-1"
>
{{ $user := didOrHandle .Did .Handle }}
{{ template "user/fragments/picHandle" $user }}
+11
appview/pages/templates/notifications/fragments/bell.html
···
+
{{define "notifications/fragments/bell"}}
+
<div class="relative"
+
hx-get="/notifications/count"
+
hx-target="#notification-count"
+
hx-trigger="load, every 30s">
+
<a href="/notifications" class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group ml-4 mr-2">
+
{{ i "bell" "w-5 h-5" }}
+
<span id="notification-count"></span>
+
</a>
+
</div>
+
{{end}}
+7
appview/pages/templates/notifications/fragments/count.html
···
+
{{define "notifications/fragments/count"}}
+
{{if and .Count (gt .Count 0)}}
+
<span class="absolute -top-1 -right-0.5 min-w-[16px] h-[16px] px-1 bg-red-500 text-white text-xs font-medium rounded-full flex items-center justify-center">
+
{{if gt .Count 99}}99+{{else}}{{.Count}}{{end}}
+
</span>
+
{{end}}
+
{{end}}
+212
appview/pages/templates/notifications/fragments/item.html
···
+
{{define "notifications/fragments/item"}}
+
<div class="border border-gray-200 dark:border-gray-700 rounded-sm p-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors {{if not .Read}}bg-blue-50 dark:bg-blue-900/20{{end}}">
+
{{if .Issue}}
+
{{template "issueNotification" .}}
+
{{else if .Pull}}
+
{{template "pullNotification" .}}
+
{{else if .Repo}}
+
{{template "repoNotification" .}}
+
{{else if eq .Type "followed"}}
+
{{template "followNotification" .}}
+
{{else}}
+
{{template "genericNotification" .}}
+
{{end}}
+
</div>
+
{{end}}
+
+
{{define "issueNotification"}}
+
{{$url := printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Name .Issue.IssueId}}
+
<a
+
href="{{$url}}"
+
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
+
>
+
<div class="flex items-center justify-between">
+
<div class="min-w-0 flex-1">
+
<!-- First line: icon + actor action -->
+
<div class="flex items-center gap-2 text-gray-900 dark:text-white">
+
{{if eq .Type "issue_created"}}
+
<span class="text-green-600 dark:text-green-500">
+
{{ i "circle-dot" "w-4 h-4" }}
+
</span>
+
{{else if eq .Type "issue_commented"}}
+
<span class="text-gray-500 dark:text-gray-400">
+
{{ i "message-circle" "w-4 h-4" }}
+
</span>
+
{{else if eq .Type "issue_closed"}}
+
<span class="text-gray-500 dark:text-gray-400">
+
{{ i "ban" "w-4 h-4" }}
+
</span>
+
{{end}}
+
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
+
{{if eq .Type "issue_created"}}
+
<span class="text-gray-500 dark:text-gray-400">opened issue</span>
+
{{else if eq .Type "issue_commented"}}
+
<span class="text-gray-500 dark:text-gray-400">commented on issue</span>
+
{{else if eq .Type "issue_closed"}}
+
<span class="text-gray-500 dark:text-gray-400">closed issue</span>
+
{{end}}
+
{{if not .Read}}
+
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
+
{{end}}
+
</div>
+
+
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5 ml-6 flex items-center gap-1">
+
<span class="text-gray-500 dark:text-gray-400">#{{.Issue.IssueId}}</span>
+
<span class="text-gray-900 dark:text-white truncate">{{.Issue.Title}}</span>
+
<span>on</span>
+
<span class="font-medium text-gray-900 dark:text-white">{{resolve .Repo.Did}}/{{.Repo.Name}}</span>
+
</div>
+
</div>
+
+
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
+
{{ template "repo/fragments/time" .Created }}
+
</div>
+
</div>
+
</a>
+
{{end}}
+
+
{{define "pullNotification"}}
+
{{$url := printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Name .Pull.PullId}}
+
<a
+
href="{{$url}}"
+
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
+
>
+
<div class="flex items-center justify-between">
+
<div class="min-w-0 flex-1">
+
<div class="flex items-center gap-2 text-gray-900 dark:text-white">
+
{{if eq .Type "pull_created"}}
+
<span class="text-green-600 dark:text-green-500">
+
{{ i "git-pull-request-create" "w-4 h-4" }}
+
</span>
+
{{else if eq .Type "pull_commented"}}
+
<span class="text-gray-500 dark:text-gray-400">
+
{{ i "message-circle" "w-4 h-4" }}
+
</span>
+
{{else if eq .Type "pull_merged"}}
+
<span class="text-purple-600 dark:text-purple-500">
+
{{ i "git-merge" "w-4 h-4" }}
+
</span>
+
{{else if eq .Type "pull_closed"}}
+
<span class="text-red-600 dark:text-red-500">
+
{{ i "git-pull-request-closed" "w-4 h-4" }}
+
</span>
+
{{end}}
+
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
+
{{if eq .Type "pull_created"}}
+
<span class="text-gray-500 dark:text-gray-400">opened pull request</span>
+
{{else if eq .Type "pull_commented"}}
+
<span class="text-gray-500 dark:text-gray-400">commented on pull request</span>
+
{{else if eq .Type "pull_merged"}}
+
<span class="text-gray-500 dark:text-gray-400">merged pull request</span>
+
{{else if eq .Type "pull_closed"}}
+
<span class="text-gray-500 dark:text-gray-400">closed pull request</span>
+
{{end}}
+
{{if not .Read}}
+
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
+
{{end}}
+
</div>
+
+
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5 ml-6 flex items-center gap-1">
+
<span class="text-gray-500 dark:text-gray-400">#{{.Pull.PullId}}</span>
+
<span class="text-gray-900 dark:text-white truncate">{{.Pull.Title}}</span>
+
<span>on</span>
+
<span class="font-medium text-gray-900 dark:text-white">{{resolve .Repo.Did}}/{{.Repo.Name}}</span>
+
</div>
+
</div>
+
+
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
+
{{ template "repo/fragments/time" .Created }}
+
</div>
+
</div>
+
</a>
+
{{end}}
+
+
{{define "repoNotification"}}
+
{{$url := printf "/%s/%s" (resolve .Repo.Did) .Repo.Name}}
+
<a
+
href="{{$url}}"
+
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
+
>
+
<div class="flex items-center justify-between">
+
<div class="flex items-center gap-2 min-w-0 flex-1">
+
<span class="text-yellow-500 dark:text-yellow-400">
+
{{ i "star" "w-4 h-4" }}
+
</span>
+
+
<div class="min-w-0 flex-1">
+
<!-- Single line for stars: actor action subject -->
+
<div class="flex items-center gap-1 text-gray-900 dark:text-white">
+
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
+
<span class="text-gray-500 dark:text-gray-400">starred</span>
+
<span class="font-medium">{{resolve .Repo.Did}}/{{.Repo.Name}}</span>
+
{{if not .Read}}
+
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
+
{{end}}
+
</div>
+
</div>
+
</div>
+
+
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
+
{{ template "repo/fragments/time" .Created }}
+
</div>
+
</div>
+
</a>
+
{{end}}
+
+
{{define "followNotification"}}
+
{{$url := printf "/%s" (resolve .ActorDid)}}
+
<a
+
href="{{$url}}"
+
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
+
>
+
<div class="flex items-center justify-between">
+
<div class="flex items-center gap-2 min-w-0 flex-1">
+
<span class="text-blue-600 dark:text-blue-400">
+
{{ i "user-plus" "w-4 h-4" }}
+
</span>
+
+
<div class="min-w-0 flex-1">
+
<div class="flex items-center gap-1 text-gray-900 dark:text-white">
+
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
+
<span class="text-gray-500 dark:text-gray-400">followed you</span>
+
{{if not .Read}}
+
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
+
{{end}}
+
</div>
+
</div>
+
</div>
+
+
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
+
{{ template "repo/fragments/time" .Created }}
+
</div>
+
</div>
+
</a>
+
{{end}}
+
+
{{define "genericNotification"}}
+
<a
+
href="#"
+
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
+
>
+
<div class="flex items-center justify-between">
+
<div class="flex items-center gap-2 min-w-0 flex-1">
+
<span class="{{if not .Read}}text-blue-600 dark:text-blue-400{{else}}text-gray-500 dark:text-gray-400{{end}}">
+
{{ i "bell" "w-4 h-4" }}
+
</span>
+
+
<div class="min-w-0 flex-1">
+
<div class="flex items-center gap-1 text-gray-900 dark:text-white">
+
<span>New notification</span>
+
{{if not .Read}}
+
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
+
{{end}}
+
</div>
+
</div>
+
</div>
+
+
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
+
{{ template "repo/fragments/time" .Created }}
+
</div>
+
</div>
+
</a>
+
{{end}}
+46
appview/pages/templates/notifications/list.html
···
+
{{ define "title" }}notifications{{ end }}
+
+
{{ define "content" }}
+
<div class="p-6">
+
<div class="flex items-center justify-between mb-4">
+
<p class="text-xl font-bold dark:text-white">Notifications</p>
+
<a href="/settings/notifications" class="flex items-center gap-2">
+
{{ i "settings" "w-4 h-4" }}
+
preferences
+
</a>
+
</div>
+
</div>
+
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
{{if .Notifications}}
+
<div class="flex flex-col gap-4" id="notifications-list">
+
{{range .Notifications}}
+
{{template "notifications/fragments/item" .}}
+
{{end}}
+
</div>
+
+
{{if .HasMore}}
+
<div class="mt-6 text-center">
+
<button
+
class="btn gap-2 group"
+
hx-get="/notifications?offset={{.NextOffset}}&limit={{.Limit}}"
+
hx-target="#notifications-list"
+
hx-swap="beforeend"
+
>
+
{{ i "chevron-down" "w-4 h-4 group-[.htmx-request]:hidden" }}
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
Load more
+
</button>
+
</div>
+
{{end}}
+
{{else}}
+
<div class="text-center py-12">
+
<div class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600">
+
{{ i "bell-off" "w-16 h-16" }}
+
</div>
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No notifications</h3>
+
<p class="text-gray-600 dark:text-gray-400">When you receive notifications, they'll appear here.</p>
+
</div>
+
{{end}}
+
</div>
+
{{ end }}
+1 -1
appview/pages/templates/strings/timeline.html
···
{{ $stat := .Stats }}
{{ $resolved := resolve .Did.String }}
<div class="text-gray-400 pt-4 text-sm font-mono inline-flex items-center gap-2 mt-auto">
-
<a href="/strings/{{ $resolved }}" class="flex items-center">
+
<a href="/strings/{{ $resolved }}" class="flex items-center gap-1">
{{ template "user/fragments/picHandle" $resolved }}
</a>
<span class="select-none [&:before]:content-['·']"></span>
+1 -1
appview/pages/templates/user/fragments/picHandle.html
···
<img
src="{{ tinyAvatar . }}"
alt=""
-
class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700"
+
class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700"
/>
{{ . | truncateAt30 }}
{{ end }}
+1 -1
appview/pages/templates/user/fragments/picHandleLink.html
···
{{ define "user/fragments/picHandleLink" }}
{{ $resolved := resolve . }}
-
<a href="/{{ $resolved }}" class="flex items-center">
+
<a href="/{{ $resolved }}" class="flex items-center gap-1">
{{ template "user/fragments/picHandle" $resolved }}
</a>
{{ end }}
+173
appview/pages/templates/user/settings/notifications.html
···
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
+
+
{{ define "content" }}
+
<div class="p-6">
+
<p class="text-xl font-bold dark:text-white">Settings</p>
+
</div>
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
+
<div class="col-span-1">
+
{{ template "user/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
+
{{ template "notificationSettings" . }}
+
</div>
+
</section>
+
</div>
+
{{ end }}
+
+
{{ define "notificationSettings" }}
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
+
<div class="col-span-1 md:col-span-2">
+
<h2 class="text-sm pb-2 uppercase font-bold">Notification Preferences</h2>
+
<p class="text-gray-500 dark:text-gray-400">
+
Choose which notifications you want to receive when activity happens on your repositories and profile.
+
</p>
+
</div>
+
</div>
+
+
<form hx-put="/settings/notifications" hx-swap="none" class="flex flex-col gap-6">
+
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full">
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">Repository starred</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When someone stars your repository.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="repo_starred" {{if .Preferences.RepoStarred}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">New issues</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When someone creates an issue on your repository.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="issue_created" {{if .Preferences.IssueCreated}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">Issue comments</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When someone comments on an issue you're involved with.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="issue_commented" {{if .Preferences.IssueCommented}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">Issue closed</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When an issue on your repository is closed.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="issue_closed" {{if .Preferences.IssueClosed}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">New pull requests</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When someone creates a pull request on your repository.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="pull_created" {{if .Preferences.PullCreated}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">Pull request comments</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When someone comments on a pull request you're involved with.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="pull_commented" {{if .Preferences.PullCommented}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">Pull request merged</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When your pull request is merged.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="pull_merged" {{if .Preferences.PullMerged}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">New followers</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>When someone follows you.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="followed" {{if .Preferences.Followed}}checked{{end}}>
+
</label>
+
</div>
+
+
<div class="flex items-center justify-between p-2">
+
<div class="flex items-center gap-2">
+
<div class="flex flex-col gap-1">
+
<span class="font-bold">Email notifications</span>
+
<div class="flex text-sm items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>Receive notifications via email in addition to in-app notifications.</span>
+
</div>
+
</div>
+
</div>
+
<label class="flex items-center gap-2">
+
<input type="checkbox" name="email_notifications" {{if .Preferences.EmailNotifications}}checked{{end}}>
+
</label>
+
</div>
+
</div>
+
+
<div class="flex justify-end pt-2">
+
<button
+
type="submit"
+
class="btn-create flex items-center gap-2 group"
+
>
+
{{ i "save" "w-4 h-4" }}
+
save
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</div>
+
<div id="settings-notifications-success"></div>
+
+
<div id="settings-notifications-error" class="error"></div>
+
</form>
+
{{ end }}