mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
Search pagination
This commit is contained in:
@@ -11,18 +11,17 @@ export class SearchController {
|
||||
|
||||
public search = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { query, filters, page, limit } = req.body as SearchQuery;
|
||||
const { keywords, page, limit } = req.query;
|
||||
|
||||
if (!query) {
|
||||
res.status(400).json({ message: 'Query is required' });
|
||||
if (!keywords) {
|
||||
res.status(400).json({ message: 'Keywords are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await this.searchService.searchEmails({
|
||||
query,
|
||||
filters,
|
||||
page,
|
||||
limit
|
||||
query: keywords as string,
|
||||
page: page ? parseInt(page as string) : 1,
|
||||
limit: limit ? parseInt(limit as string) : 10
|
||||
});
|
||||
|
||||
res.status(200).json(results);
|
||||
|
||||
@@ -11,7 +11,7 @@ export const createSearchRouter = (
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
router.post('/', searchController.search);
|
||||
router.get('/', searchController.search);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -62,7 +62,8 @@ export class SearchService {
|
||||
total: searchResults.estimatedTotalHits ?? searchResults.hits.length,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil((searchResults.estimatedTotalHits ?? searchResults.hits.length) / limit)
|
||||
totalPages: Math.ceil((searchResults.estimatedTotalHits ?? searchResults.hits.length) / limit),
|
||||
processingTimeMs: searchResults.processingTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
import type { PageServerLoad, Actions, RequestEvent } from './$types';
|
||||
import { api } from '$lib/server/api';
|
||||
import type { SearchResult } from '@open-archiver/types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
async function performSearch(query: string, event: RequestEvent) {
|
||||
if (!query) {
|
||||
return { searchResult: null, query: '' };
|
||||
async function performSearch(keywords: string, page: number, event: RequestEvent) {
|
||||
if (!keywords) {
|
||||
return { searchResult: null, keywords: '', page: 1 };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api('/search', event, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query })
|
||||
const response = await api(`/search?keywords=${keywords}&page=${page}&limit=10`, event, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
return { searchResult: null, query, error: error.message };
|
||||
return { searchResult: null, keywords, page, error: error.message };
|
||||
}
|
||||
|
||||
const searchResult = (await response.json()) as SearchResult;
|
||||
return { searchResult, query };
|
||||
return { searchResult, keywords, page };
|
||||
} catch (error) {
|
||||
return {
|
||||
searchResult: null,
|
||||
query,
|
||||
keywords,
|
||||
page,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const query = event.url.searchParams.get('query') || '';
|
||||
return performSearch(query, event);
|
||||
const keywords = event.url.searchParams.get('keywords') || '';
|
||||
const page = parseInt(event.url.searchParams.get('page') || '1');
|
||||
return performSearch(keywords, page, event);
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const query = formData.get('query') as string;
|
||||
return performSearch(query, event);
|
||||
const keywords = formData.get('keywords') as string;
|
||||
|
||||
if (keywords) {
|
||||
throw redirect(303, `/dashboard/search?keywords=${keywords}`);
|
||||
}
|
||||
|
||||
return {
|
||||
searchResult: null,
|
||||
keywords: '',
|
||||
page: 1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import type { PageData } from './$types';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import {
|
||||
@@ -12,10 +12,11 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
const searchResult = form?.searchResult ?? data.searchResult;
|
||||
const query = form?.query ?? data.query;
|
||||
const error = form?.error;
|
||||
let { data }: { data: PageData } = $props();
|
||||
let searchResult = $derived(data.searchResult);
|
||||
let keywords = $derived(data.keywords);
|
||||
let page = $derived(data.page);
|
||||
let error = $derived(data.error);
|
||||
|
||||
let isMounted = $state(false);
|
||||
onMount(() => {
|
||||
@@ -91,6 +92,52 @@
|
||||
|
||||
return snippets;
|
||||
}
|
||||
|
||||
const getPaginationItems = (currentPage: number, totalPages: number, siblingCount = 1) => {
|
||||
const totalPageNumbers = siblingCount + 5;
|
||||
|
||||
if (totalPages <= totalPageNumbers) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
|
||||
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages);
|
||||
|
||||
const shouldShowLeftDots = leftSiblingIndex > 2;
|
||||
const shouldShowRightDots = rightSiblingIndex < totalPages - 2;
|
||||
|
||||
const firstPageIndex = 1;
|
||||
const lastPageIndex = totalPages;
|
||||
|
||||
if (!shouldShowLeftDots && shouldShowRightDots) {
|
||||
let leftItemCount = 3 + 2 * siblingCount;
|
||||
let leftRange = Array.from({ length: leftItemCount }, (_, i) => i + 1);
|
||||
return [...leftRange, '...', totalPages];
|
||||
}
|
||||
|
||||
if (shouldShowLeftDots && !shouldShowRightDots) {
|
||||
let rightItemCount = 3 + 2 * siblingCount;
|
||||
let rightRange = Array.from(
|
||||
{ length: rightItemCount },
|
||||
(_, i) => totalPages - rightItemCount + i + 1
|
||||
);
|
||||
return [firstPageIndex, '...', ...rightRange];
|
||||
}
|
||||
|
||||
if (shouldShowLeftDots && shouldShowRightDots) {
|
||||
let middleRange = Array.from(
|
||||
{ length: rightSiblingIndex - leftSiblingIndex + 1 },
|
||||
(_, i) => leftSiblingIndex + i
|
||||
);
|
||||
return [firstPageIndex, '...', ...middleRange, '...', lastPageIndex];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
let paginationItems = $derived(
|
||||
getPaginationItems(page, Math.ceil((searchResult?.total || 0) / (searchResult?.limit || 10)))
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -101,13 +148,13 @@
|
||||
<div class="container mx-auto p-4 md:p-8">
|
||||
<h1 class="mb-4 text-2xl font-bold">Email Search</h1>
|
||||
|
||||
<form method="POST" action="/dashboard/search" class="mb-8 flex items-center gap-2">
|
||||
<form method="POST" action="/dashboard/search?action=search" class="mb-8 flex items-center gap-2">
|
||||
<Input
|
||||
type="search"
|
||||
name="query"
|
||||
name="keywords"
|
||||
placeholder="Search by keyword, sender, recipient..."
|
||||
class="flex-grow"
|
||||
value={query}
|
||||
value={keywords}
|
||||
/>
|
||||
<Button type="submit">Search</Button>
|
||||
</form>
|
||||
@@ -119,7 +166,7 @@
|
||||
{#if searchResult}
|
||||
<p class="text-muted-foreground mb-4">
|
||||
{#if searchResult.total > 0}
|
||||
Found {searchResult.total} results in {searchResult.hits.length / 1000}s
|
||||
Found {searchResult.total} results in {searchResult.processingTimeMs / 1000}s
|
||||
{:else}
|
||||
Found {searchResult.total} results
|
||||
{/if}
|
||||
@@ -204,5 +251,38 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if searchResult.total > searchResult.limit}
|
||||
<div class="mt-8 flex flex-row items-center justify-center space-x-2">
|
||||
<a
|
||||
href={`/dashboard/search?keywords=${keywords}&page=${page - 1}`}
|
||||
class={page === 1 ? 'pointer-events-none' : ''}
|
||||
>
|
||||
<Button variant="outline" disabled={page === 1}>Prev</Button>
|
||||
</a>
|
||||
|
||||
{#each paginationItems as item}
|
||||
{#if typeof item === 'number'}
|
||||
<a href={`/dashboard/search?keywords=${keywords}&page=${item}`}>
|
||||
<Button variant={item === page ? 'default' : 'outline'}>{item}</Button>
|
||||
</a>
|
||||
{:else}
|
||||
<span class="px-4 py-2">...</span>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<a
|
||||
href={`/dashboard/search?keywords=${keywords}&page=${page + 1}`}
|
||||
class={page === Math.ceil(searchResult.total / searchResult.limit)
|
||||
? 'pointer-events-none'
|
||||
: ''}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={page === Math.ceil(searchResult.total / searchResult.limit)}>Next</Button
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -20,4 +20,5 @@ export interface SearchResult {
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
processingTimeMs: number;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user