Add notification pagination to backend and frontend
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<div class="flex gap-2">
|
||||||
<button class="bg-blue-500 text-white px-4 py-2 rounded" on:click={markAllViewed}>
|
<button class="bg-blue-500 text-white px-4 py-2 rounded" on:click={markAllViewed}>
|
||||||
Mark All Viewed
|
Mark All Viewed
|
||||||
</button>
|
</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">
|
||||||
|
|||||||
Reference in New Issue
Block a user