Add notification pagination to backend and frontend

This commit is contained in:
Sami Abuzakuk
2025-10-22 22:37:12 +02:00
parent d3df001397
commit 16989ed518
3 changed files with 82 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from fastapi import FastAPI from fastapi import FastAPI, Query
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
@@ -144,11 +144,18 @@ def remove_subscription(subscription_id: int):
@app.get("/subscriptions/{subscription_id}/notifications") @app.get("/subscriptions/{subscription_id}/notifications")
def list_subscription_notifications(subscription_id: int): def list_subscription_notifications(
subscription_id: int,
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
):
db = SessionLocal() db = SessionLocal()
notifications = ( notifications = (
db.query(Notification) db.query(Notification)
.filter(Notification.subscription_id == subscription_id) .filter(Notification.subscription_id == subscription_id)
.order_by(Notification.created_at.desc())
.limit(limit)
.offset(offset)
.all() .all()
) )
db.close() db.close()

View File

@@ -205,11 +205,15 @@ export async function deleteSubscription(subscriptionId: number): Promise<void>
} }
} }
// Get all subscription notifications // Get subscription notifications with pagination
export async function fetchSubscriptionNotifications( export async function fetchSubscriptionNotifications(
subscriptionId: string subscriptionId: string,
limit: number = 20,
offset: number = 0
): Promise<Notification[]> { ): Promise<Notification[]> {
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}/notifications`); const response = await fetch(
`${API_URL}/subscriptions/${subscriptionId}/notifications?limit=${limit}&offset=${offset}`
);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch subscription notifications'); throw new Error('Failed to fetch subscription notifications');
} }

View File

@@ -1,15 +1,34 @@
<script lang="ts"> <script lang="ts">
import { deleteNotification, addNotification, setViewed } from '$lib/api'; import { deleteNotification, setViewed, fetchSubscriptionNotifications } from '$lib/api';
import type { Notification, Subscription } from '$lib/api'; import type { Notification, Subscription } from '$lib/api';
export let data: { notifications: Notification[]; subscription: Subscription }; export let data: { notifications: Notification[]; subscription: Subscription };
let notifications: Notification[] = data.notifications; let notifications: Notification[] = data.notifications;
let newNotificationTitle = '';
let newNotificationMessage = '';
let newNotificationPriority = 3;
let selectedNotification: Notification | null = null; let selectedNotification: Notification | null = null;
// Delete all notifications for this subscription
async function handleDeleteAllNotifications() {
if (notifications.length === 0) return;
const confirmed = window.confirm(
'Are you sure you want to delete all notifications for this subscription?'
);
if (!confirmed) return;
try {
await Promise.all(notifications.map((notification) => deleteNotification(notification.id)));
notifications = [];
window.showNotification('success', 'All notifications deleted successfully.');
} catch (error) {
window.showNotification('error', 'Failed to delete all notifications - ' + error);
}
}
// Pagination state
let limit = 20;
let offset = notifications.length;
let loadingMore = false;
let allLoaded = notifications.length < limit;
async function openNotificationPopup(notification: Notification) { async function openNotificationPopup(notification: Notification) {
if (!notification.viewed) { if (!notification.viewed) {
await setViewed(notification.id); await setViewed(notification.id);
@@ -50,6 +69,26 @@
window.showNotification('error', 'Failed to mark all notifications as viewed.'); window.showNotification('error', 'Failed to mark all notifications as viewed.');
} }
} }
// Load more notifications
async function loadMoreNotifications() {
loadingMore = true;
try {
const more = await fetchSubscriptionNotifications(
data.subscription.id.toString(),
limit,
offset
);
notifications = [...notifications, ...more];
offset += more.length;
if (more.length < limit) {
allLoaded = true;
}
} catch (error) {
window.showNotification('error', 'Failed to load more notifications');
}
loadingMore = false;
}
</script> </script>
<main class="p-4"> <main class="p-4">
@@ -57,9 +96,18 @@
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a> <a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a>
<button class="bg-blue-500 text-white px-4 py-2 rounded" on:click={markAllViewed}> <div class="flex gap-2">
Mark All Viewed <button class="bg-blue-500 text-white px-4 py-2 rounded" on:click={markAllViewed}>
</button> Mark All Viewed
</button>
<button
class="bg-red-500 text-white px-4 py-2 rounded"
on:click={handleDeleteAllNotifications}
disabled={notifications.length === 0}
>
Delete All Notifications
</button>
</div>
</div> </div>
{#if notifications.length === 0} {#if notifications.length === 0}
@@ -89,6 +137,17 @@
{/each} {/each}
</ul> </ul>
{/if} {/if}
{#if !allLoaded && notifications.length > 0}
<div class="flex justify-center mt-6">
<button
class="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
on:click={loadMoreNotifications}
disabled={loadingMore}
>
{loadingMore ? 'Loading...' : 'Load More'}
</button>
</div>
{/if}
{#if selectedNotification} {#if selectedNotification}
<div class="fixed inset-0 bg-opacity-30 backdrop-blur-sm flex items-center justify-center z-50"> <div class="fixed inset-0 bg-opacity-30 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-white p-6 rounded shadow-lg w-3/4 max-w-2xl max-h-[80vh] overflow-y-auto"> <div class="bg-white p-6 rounded shadow-lg w-3/4 max-w-2xl max-h-[80vh] overflow-y-auto">