fix: Migrate over to new pagination format in dashboard

This commit is contained in:
Dries Augustyns
2026-01-02 10:40:39 +01:00
parent 49824aff93
commit 8790c45edc
7 changed files with 26 additions and 68 deletions
+1 -7
View File
@@ -77,13 +77,7 @@ export class Campaigns {
pageSize, pageSize,
}); });
return res.json({ return res.json(result);
campaigns: result.campaigns,
page: result.page,
pageSize: result.pageSize,
total: result.total,
totalPages: result.totalPages,
});
} }
/** /**
+3 -3
View File
@@ -1,6 +1,6 @@
import type {Campaign, Contact, Prisma} from '@plunk/db'; import type {Campaign, Contact, Prisma} from '@plunk/db';
import {CampaignAudienceType, CampaignStatus, EmailSourceType} from '@plunk/db'; import {CampaignAudienceType, CampaignStatus, EmailSourceType} from '@plunk/db';
import type {CreateCampaignData, FilterCondition, UpdateCampaignData} from '@plunk/types'; import type {CreateCampaignData, FilterCondition, PaginatedResponse, UpdateCampaignData} from '@plunk/types';
import {fromPrismaJson, toPrismaJson} from '@plunk/types'; import {fromPrismaJson, toPrismaJson} from '@plunk/types';
import signale from 'signale'; import signale from 'signale';
@@ -184,7 +184,7 @@ export class CampaignService {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
} = {}, } = {},
): Promise<{campaigns: Campaign[]; total: number; page: number; pageSize: number; totalPages: number}> { ): Promise<PaginatedResponse<Campaign>> {
const {status, page = 1, pageSize = 20} = options; const {status, page = 1, pageSize = 20} = options;
const skip = (page - 1) * pageSize; const skip = (page - 1) * pageSize;
@@ -207,7 +207,7 @@ export class CampaignService {
]); ]);
return { return {
campaigns, data: campaigns,
total, total,
page, page,
pageSize, pageSize,
@@ -21,18 +21,11 @@ import {
} from '@plunk/ui'; } from '@plunk/ui';
import type {Campaign} from '@plunk/db'; import type {Campaign} from '@plunk/db';
import {CampaignStatus} from '@plunk/db'; import {CampaignStatus} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types';
import {ArrowLeft, Calendar, Mail, Users} from 'lucide-react'; import {ArrowLeft, Calendar, Mail, Users} from 'lucide-react';
import {useState} from 'react'; import {useState} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
interface PaginatedCampaigns {
campaigns: Campaign[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
interface CampaignSelectionDialogProps { interface CampaignSelectionDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@@ -62,7 +55,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
audience: true, audience: true,
}); });
const {data, isLoading} = useSWR<PaginatedCampaigns>( const {data, isLoading} = useSWR<PaginatedResponse<Campaign>>(
open ? `/campaigns?page=${page}&pageSize=10${statusFilter !== 'ALL' ? `&status=${statusFilter}` : ''}` : null, open ? `/campaigns?page=${page}&pageSize=10${statusFilter !== 'ALL' ? `&status=${statusFilter}` : ''}` : null,
{revalidateOnFocus: false}, {revalidateOnFocus: false},
); );
@@ -192,7 +185,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
</div> </div>
)} )}
{!isLoading && data?.campaigns.length === 0 && ( {!isLoading && data?.data.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<Mail className="h-12 w-12 text-neutral-400 mx-auto mb-4" /> <Mail className="h-12 w-12 text-neutral-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-neutral-900 mb-2">No campaigns found</h3> <h3 className="text-lg font-medium text-neutral-900 mb-2">No campaigns found</h3>
@@ -202,7 +195,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
</div> </div>
)} )}
{data?.campaigns.map(campaign => ( {data?.data.map(campaign => (
<Card <Card
key={campaign.id} key={campaign.id}
className="cursor-pointer hover:border-primary/50 hover:shadow-md transition-all" className="cursor-pointer hover:border-primary/50 hover:shadow-md transition-all"
@@ -16,18 +16,11 @@ import {
Label, Label,
} from '@plunk/ui'; } from '@plunk/ui';
import type {Template} from '@plunk/db'; import type {Template} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types';
import {ArrowLeft, FileText, Search} from 'lucide-react'; import {ArrowLeft, FileText, Search} from 'lucide-react';
import {useState} from 'react'; import {useState} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
interface PaginatedTemplates {
templates: Template[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
interface TemplateSelectionDialogProps { interface TemplateSelectionDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@@ -57,7 +50,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
replyTo: true, replyTo: true,
}); });
const {data, isLoading} = useSWR<PaginatedTemplates>( const {data, isLoading} = useSWR<PaginatedResponse<Template>>(
open open
? `/templates?page=${page}&pageSize=10${search ? `&search=${search}` : ''}${typeFilter !== 'ALL' ? `&type=${typeFilter}` : ''}` ? `/templates?page=${page}&pageSize=10${search ? `&search=${search}` : ''}${typeFilter !== 'ALL' ? `&type=${typeFilter}` : ''}`
: null, : null,
@@ -219,7 +212,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
</div> </div>
)} )}
{!isLoading && data?.templates.length === 0 && ( {!isLoading && data?.data.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<FileText className="h-12 w-12 text-neutral-400 mx-auto mb-4" /> <FileText className="h-12 w-12 text-neutral-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-neutral-900 mb-2">No templates found</h3> <h3 className="text-lg font-medium text-neutral-900 mb-2">No templates found</h3>
@@ -229,7 +222,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
</div> </div>
)} )}
{data?.templates.map(template => ( {data?.data.map(template => (
<Card <Card
key={template.id} key={template.id}
className="cursor-pointer hover:border-primary/50 hover:shadow-md transition-all" className="cursor-pointer hover:border-primary/50 hover:shadow-md transition-all"
+4 -11
View File
@@ -19,6 +19,7 @@ import {
} from '@plunk/ui'; } from '@plunk/ui';
import type {Campaign, Template} from '@plunk/db'; import type {Campaign, Template} from '@plunk/db';
import {CampaignStatus} from '@plunk/db'; import {CampaignStatus} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {TemplateSelectionDialog} from '../../components/TemplateSelectionDialog'; import {TemplateSelectionDialog} from '../../components/TemplateSelectionDialog';
import {CampaignSelectionDialog} from '../../components/CampaignSelectionDialog'; import {CampaignSelectionDialog} from '../../components/CampaignSelectionDialog';
@@ -33,14 +34,6 @@ import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface PaginatedCampaigns {
campaigns: Campaign[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export default function CampaignsPage() { export default function CampaignsPage() {
const router = useRouter(); const router = useRouter();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@@ -52,7 +45,7 @@ export default function CampaignsPage() {
const [showTemplateDialog, setShowTemplateDialog] = useState(false); const [showTemplateDialog, setShowTemplateDialog] = useState(false);
const [showCampaignDialog, setShowCampaignDialog] = useState(false); const [showCampaignDialog, setShowCampaignDialog] = useState(false);
const {data, mutate, isLoading} = useSWR<PaginatedCampaigns>( const {data, mutate, isLoading} = useSWR<PaginatedResponse<Campaign>>(
`/campaigns?page=${page}&pageSize=20${statusFilter !== 'ALL' ? `&status=${statusFilter}` : ''}`, `/campaigns?page=${page}&pageSize=20${statusFilter !== 'ALL' ? `&status=${statusFilter}` : ''}`,
{revalidateOnFocus: false}, {revalidateOnFocus: false},
); );
@@ -293,7 +286,7 @@ export default function CampaignsPage() {
</Card> </Card>
)} )}
{!isLoading && data?.campaigns.length === 0 && ( {!isLoading && data?.data.length === 0 && (
<Card> <Card>
<CardContent className="py-16 text-center"> <CardContent className="py-16 text-center">
<div className="max-w-md mx-auto"> <div className="max-w-md mx-auto">
@@ -367,7 +360,7 @@ export default function CampaignsPage() {
</Card> </Card>
)} )}
{data?.campaigns.map(campaign => { {data?.data.map(campaign => {
const openRate = campaign.sentCount > 0 ? (campaign.openedCount / campaign.sentCount) * 100 : 0; const openRate = campaign.sentCount > 0 ? (campaign.openedCount / campaign.sentCount) * 100 : 0;
const clickRate = campaign.sentCount > 0 ? (campaign.clickedCount / campaign.sentCount) * 100 : 0; const clickRate = campaign.sentCount > 0 ? (campaign.clickedCount / campaign.sentCount) * 100 : 0;
const deliveryProgress = const deliveryProgress =
+4 -11
View File
@@ -10,6 +10,7 @@ import {
Input, Input,
} from '@plunk/ui'; } from '@plunk/ui';
import type {Template} from '@plunk/db'; import type {Template} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {formatRelativeTime} from '../../lib/dateUtils'; import {formatRelativeTime} from '../../lib/dateUtils';
@@ -21,14 +22,6 @@ import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface PaginatedTemplates {
templates: Template[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export default function TemplatesPage() { export default function TemplatesPage() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -37,7 +30,7 @@ export default function TemplatesPage() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<string | null>(null); const [templateToDelete, setTemplateToDelete] = useState<string | null>(null);
const {data, mutate, isLoading} = useSWR<PaginatedTemplates>( const {data, mutate, isLoading} = useSWR<PaginatedResponse<Template>>(
`/templates?page=${page}&pageSize=20${search ? `&search=${search}` : ''}${typeFilter !== 'ALL' ? `&type=${typeFilter}` : ''}`, `/templates?page=${page}&pageSize=20${search ? `&search=${search}` : ''}${typeFilter !== 'ALL' ? `&type=${typeFilter}` : ''}`,
{revalidateOnFocus: false}, {revalidateOnFocus: false},
); );
@@ -182,7 +175,7 @@ export default function TemplatesPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) : data?.templates.length === 0 ? ( ) : data?.data.length === 0 ? (
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-center py-12"> <div className="text-center py-12">
@@ -204,7 +197,7 @@ export default function TemplatesPage() {
</Card> </Card>
) : ( ) : (
<> <>
{data?.templates.map(template => ( {data?.data.map(template => (
<Card key={template.id}> <Card key={template.id}>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
+6 -14
View File
@@ -21,6 +21,7 @@ import {
SelectValue, SelectValue,
} from '@plunk/ui'; } from '@plunk/ui';
import type {Workflow} from '@plunk/db'; import type {Workflow} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {formatRelativeTime} from '../../lib/dateUtils'; import {formatRelativeTime} from '../../lib/dateUtils';
@@ -33,14 +34,6 @@ import useSWR from 'swr';
import {WorkflowSchemas} from '@plunk/shared'; import {WorkflowSchemas} from '@plunk/shared';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface PaginatedWorkflows {
workflows: (Workflow & {_count?: {steps: number; executions: number}})[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export default function WorkflowsPage() { export default function WorkflowsPage() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -49,10 +42,9 @@ export default function WorkflowsPage() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [workflowToDelete, setWorkflowToDelete] = useState<string | null>(null); const [workflowToDelete, setWorkflowToDelete] = useState<string | null>(null);
const {data, mutate, isLoading} = useSWR<PaginatedWorkflows>( const {data, mutate, isLoading} = useSWR<
`/workflows?page=${page}&pageSize=20${search ? `&search=${search}` : ''}`, PaginatedResponse<Workflow & {_count?: {steps: number; executions: number}}>
{revalidateOnFocus: false}, >(`/workflows?page=${page}&pageSize=20${search ? `&search=${search}` : ''}`, {revalidateOnFocus: false});
);
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -164,7 +156,7 @@ export default function WorkflowsPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) : data?.workflows.length === 0 ? ( ) : data?.data.length === 0 ? (
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-center py-12"> <div className="text-center py-12">
@@ -184,7 +176,7 @@ export default function WorkflowsPage() {
</Card> </Card>
) : ( ) : (
<> <>
{data?.workflows.map(workflow => ( {data?.data.map(workflow => (
<Card key={workflow.id} className={workflow.enabled ? 'border-green-200' : ''}> <Card key={workflow.id} className={workflow.enabled ? 'border-green-200' : ''}>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">