✨ Notifications
This commit is contained in:
		| @@ -9,5 +9,6 @@ type Notification struct { | |||||||
| 	Content     string     `json:"content"` | 	Content     string     `json:"content"` | ||||||
| 	IsImportant bool       `json:"is_important"` | 	IsImportant bool       `json:"is_important"` | ||||||
| 	ReadAt      *time.Time `json:"read_at"` | 	ReadAt      *time.Time `json:"read_at"` | ||||||
|  | 	SenderID    *uint      `json:"sender_id"` | ||||||
| 	RecipientID uint       `json:"recipient_id"` | 	RecipientID uint       `json:"recipient_id"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ func getUserinfo(c *fiber.Ctx) error { | |||||||
| 		Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}). | 		Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}). | ||||||
| 		Preload("Profile"). | 		Preload("Profile"). | ||||||
| 		Preload("Contacts"). | 		Preload("Contacts"). | ||||||
| 		Preload("Factors"). |  | ||||||
| 		Preload("Notifications", "read_at IS NULL"). | 		Preload("Notifications", "read_at IS NULL"). | ||||||
| 		First(&data).Error; err != nil { | 		First(&data).Error; err != nil { | ||||||
| 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								pkg/server/notifications_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/server/notifications_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | package server | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"code.smartsheep.studio/hydrogen/passport/pkg/database" | ||||||
|  | 	"code.smartsheep.studio/hydrogen/passport/pkg/models" | ||||||
|  | 	"github.com/gofiber/fiber/v2" | ||||||
|  | 	"github.com/samber/lo" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func getNotifications(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  | 	take := c.QueryInt("take", 0) | ||||||
|  | 	offset := c.QueryInt("offset", 0) | ||||||
|  |  | ||||||
|  | 	var count int64 | ||||||
|  | 	var notifications []models.Notification | ||||||
|  | 	if err := database.C. | ||||||
|  | 		Where(&models.Notification{RecipientID: user.ID}). | ||||||
|  | 		Model(&models.Notification{}). | ||||||
|  | 		Count(&count).Error; err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := database.C. | ||||||
|  | 		Where(&models.Notification{RecipientID: user.ID}). | ||||||
|  | 		Limit(take). | ||||||
|  | 		Offset(offset). | ||||||
|  | 		Order("read_at desc"). | ||||||
|  | 		Find(¬ifications).Error; err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(fiber.Map{ | ||||||
|  | 		"count": count, | ||||||
|  | 		"data":  notifications, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func markNotificationRead(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  | 	id, _ := c.ParamsInt("notificationId", 0) | ||||||
|  |  | ||||||
|  | 	var data models.Notification | ||||||
|  | 	if err := database.C.Where(&models.Notification{ | ||||||
|  | 		BaseModel:   models.BaseModel{ID: uint(id)}, | ||||||
|  | 		RecipientID: user.ID, | ||||||
|  | 	}).First(&data).Error; err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data.ReadAt = lo.ToPtr(time.Now()) | ||||||
|  |  | ||||||
|  | 	if err := database.C.Save(&data).Error; err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} else { | ||||||
|  | 		return c.SendStatus(fiber.StatusOK) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -60,6 +60,9 @@ func NewServer() { | |||||||
| 		api.Get("/avatar/:avatarId", getAvatar) | 		api.Get("/avatar/:avatarId", getAvatar) | ||||||
| 		api.Put("/avatar", auth, setAvatar) | 		api.Put("/avatar", auth, setAvatar) | ||||||
|  |  | ||||||
|  | 		api.Get("/notifications", auth, getNotifications) | ||||||
|  | 		api.Put("/notifications/:notificationId/read", auth, markNotificationRead) | ||||||
|  |  | ||||||
| 		api.Get("/users/me", auth, getUserinfo) | 		api.Get("/users/me", auth, getUserinfo) | ||||||
| 		api.Put("/users/me", auth, editUserinfo) | 		api.Put("/users/me", auth, editUserinfo) | ||||||
| 		api.Get("/users/me/events", auth, getEvents) | 		api.Get("/users/me/events", auth, getEvents) | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| import { useUserinfo } from "../stores/userinfo.tsx"; | import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx"; | ||||||
| import { Show } from "solid-js"; | import { createSignal, For, Show } from "solid-js"; | ||||||
|  |  | ||||||
| export default function DashboardPage() { | export default function DashboardPage() { | ||||||
|   const userinfo = useUserinfo(); |   const userinfo = useUserinfo(); | ||||||
|  |  | ||||||
|  |   const [error, setError] = createSignal<string | null>(null); | ||||||
|  |  | ||||||
|   function getGreeting() { |   function getGreeting() { | ||||||
|     const currentHour = new Date().getHours(); |     const currentHour = new Date().getHours(); | ||||||
|  |  | ||||||
| @@ -16,6 +18,19 @@ export default function DashboardPage() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async function readNotification(item: any) { | ||||||
|  |     const res = await fetch(`/api/notifications/${item.id}/read`, { | ||||||
|  |       method: "PUT", | ||||||
|  |       headers: { Authorization: `Bearer ${getAtk()}` } | ||||||
|  |     }); | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       setError(await res.text()); | ||||||
|  |     } else { | ||||||
|  |       await readProfiles(); | ||||||
|  |       setError(null); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div class="max-w-[720px] mx-auto px-5 pt-12"> |     <div class="max-w-[720px] mx-auto px-5 pt-12"> | ||||||
|       <div id="greeting" class="px-5"> |       <div id="greeting" class="px-5"> | ||||||
| @@ -37,17 +52,54 @@ export default function DashboardPage() { | |||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </Show> |         </Show> | ||||||
|  |         <Show when={error()}> | ||||||
|  |           <div role="alert" class="alert alert-error mt-5"> | ||||||
|  |             <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" | ||||||
|  |                  viewBox="0 0 24 24"> | ||||||
|  |               <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||||
|  |                     d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||||||
|  |             </svg> | ||||||
|  |             <span class="capitalize">{error()}</span> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="card shadow-xl mt-5"> |       <div class="card shadow-xl mt-5"> | ||||||
|         <div class="card-body"> |         <div class="card-body"> | ||||||
|           <h2 class="card-title">Recommendations</h2> |           <h2 class="card-title">Notifications</h2> | ||||||
|           <ol> |           <div class="bg-base-200 mt-3 mx-[-32px]"> | ||||||
|             <li>1. Turn on the notification of us.</li> |             <Show when={userinfo?.meta?.notifications?.length <= 0}> | ||||||
|             <li>2. Download passport mobile application.</li> |               <table class="table"> | ||||||
|             <li>3. Add more factor to keep your account in safe.</li> |                 <tbody> | ||||||
|             <li>4. Subscribe to Project Hydrogen to get following updates.</li> |                 <tr> | ||||||
|           </ol> |                   <td class="px-[32px]">You're done! There are no notifications unread for you.</td> | ||||||
|  |                 </tr> | ||||||
|  |                 </tbody> | ||||||
|  |               </table> | ||||||
|  |             </Show> | ||||||
|  |             <Show when={userinfo?.meta?.notifications?.length > 0}> | ||||||
|  |               <table class="table"> | ||||||
|  |                 <tbody> | ||||||
|  |                 <For each={userinfo?.meta?.notifications}> | ||||||
|  |                   {item => | ||||||
|  |                     <tr> | ||||||
|  |                       <td class="px-[32px]"> | ||||||
|  |                         <h2 class="font-bold">{item.subject}</h2> | ||||||
|  |                         <p>{item.content}</p> | ||||||
|  |                         <div class="flex gap-2"> | ||||||
|  |                           <Show when={item.is_important}> | ||||||
|  |                             <span class="font-bold">Important</span> | ||||||
|  |                           </Show> | ||||||
|  |                           <a class="link" onClick={() => readNotification(item)}>Mark as read</a> | ||||||
|  |                         </div> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                   } | ||||||
|  |                 </For> | ||||||
|  |                 </tbody> | ||||||
|  |               </table> | ||||||
|  |             </Show> | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user