ui: replace custom loading spinners with IconSpinner component across multiple files

This commit is contained in:
Dries Augustyns
2026-04-21 16:28:03 +02:00
parent c278a0fd94
commit 60146fcc10
29 changed files with 98 additions and 488 deletions
+4 -4
View File
@@ -1,9 +1,9 @@
import {Button} from '@plunk/ui';
import {Button, IconSpinner} from '@plunk/ui';
import type {Activity, CursorPaginatedResponse} from '@plunk/types';
import {network} from '../lib/network';
import {ActivityItem} from './ActivityItem';
import {EmptyState} from './EmptyState';
import {Activity as ActivityIcon, Loader2} from 'lucide-react';
import {Activity as ActivityIcon} from 'lucide-react';
import {useCallback, useEffect, useMemo, useState} from 'react';
export interface ActivityFeedProps {
@@ -169,7 +169,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-neutral-400" />
<IconSpinner />
</div>
);
}
@@ -215,7 +215,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
<Button onClick={loadMore} variant="outline" disabled={isLoadingMore}>
{isLoadingMore ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
<IconSpinner size="sm" className="mr-2" />
Loading...
</>
) : (
@@ -1,4 +1,4 @@
import {Alert, Card, CardContent, CardDescription, CardHeader, CardTitle} from '@plunk/ui';
import {Alert, Card, CardContent, CardDescription, CardHeader, CardTitle, IconSpinner} from '@plunk/ui';
import {AlertCircle, Coins, TrendingUp} from 'lucide-react';
import {useBillingConsumption} from '../lib/hooks/useBillingConsumption';
import {useConfig} from '../lib/hooks/useConfig';
@@ -67,7 +67,9 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt
<CardDescription>View your current month email consumption and costs</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-neutral-500">Loading...</p>
<div className="flex items-center justify-center py-4">
<IconSpinner size="sm" />
</div>
</CardContent>
</Card>
);
+4 -2
View File
@@ -1,4 +1,4 @@
import {Alert, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle} from '@plunk/ui';
import {Alert, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, IconSpinner} from '@plunk/ui';
import {AlertCircle, Download, ExternalLink, FileText} from 'lucide-react';
import {useBillingInvoices} from '../lib/hooks/useBillingInvoices';
@@ -41,7 +41,9 @@ export function BillingInvoices({projectId, hasSubscription, onManageBilling}: B
<CardDescription>View and download your billing history</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-neutral-500">Loading...</p>
<div className="flex items-center justify-center py-4">
<IconSpinner size="sm" />
</div>
</CardContent>
</Card>
);
+4 -1
View File
@@ -13,6 +13,7 @@ import {
Form,
FormControl,
FormDescription,
IconSpinner,
FormField,
FormItem,
FormLabel,
@@ -146,7 +147,9 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
<CardDescription>Set monthly limits for each email category</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-neutral-500">Loading...</p>
<div className="flex items-center justify-center py-4">
<IconSpinner size="sm" />
</div>
</CardContent>
</Card>
);
@@ -17,6 +17,7 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
IconSpinner,
SelectValue,
} from '@plunk/ui';
import type {Campaign} from '@plunk/db';
@@ -167,22 +168,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
<div className="flex-1 overflow-y-auto space-y-3">
{isLoading && (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading campaigns...</p>
</div>
<IconSpinner />
</div>
)}
@@ -20,8 +20,9 @@ import {
TableHead,
TableHeader,
TableRow,
IconSpinner,
} from '@plunk/ui';
import {AlertCircle, Database, Loader2, Trash2, Zap} from 'lucide-react';
import {AlertCircle, Database, Trash2, Zap} from 'lucide-react';
import {EmptyState} from './EmptyState';
import {toast} from 'sonner';
import useSWR from 'swr';
@@ -345,7 +346,7 @@ export function DataManagementSettings() {
isDeleting || (!!selectedField && !fieldUsage?.canDelete) || (!!selectedEvent && !eventUsage?.canDelete)
}
>
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isDeleting && <IconSpinner size="sm" className="mr-2" />}
Delete
</Button>
</DialogFooter>
+4 -3
View File
@@ -17,10 +17,11 @@ import {
FormItem,
FormLabel,
FormMessage,
IconSpinner,
Input,
} from '@plunk/ui';
import {AnimatePresence, motion} from 'framer-motion';
import {Check, CheckCircle2, ChevronDown, Copy, Globe, Loader2, RefreshCw, Trash2, XCircle} from 'lucide-react';
import {Check, CheckCircle2, ChevronDown, Copy, Globe, RefreshCw, Trash2, XCircle} from 'lucide-react';
import {EmptyState} from './EmptyState';
import {useConfig} from '../lib/hooks/useConfig';
import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains';
@@ -322,7 +323,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-neutral-400" />
<IconSpinner />
</div>
) : !domains || domains.length === 0 ? (
<EmptyState
@@ -360,7 +361,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
className="min-w-[80px]"
>
{checkingVerification === domain.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
<IconSpinner size="sm" />
) : (cooldownSeconds[domain.id] ?? 0) > 0 ? (
<span className="text-xs">{cooldownSeconds[domain.id]}s</span>
) : (
+2 -1
View File
@@ -37,6 +37,7 @@ import {
TableHead,
TableHeader,
TableRow,
IconSpinner,
} from '@plunk/ui';
import {MembershipSchemas} from '@plunk/shared';
import {MoreVertical, Trash2, UserPlus} from 'lucide-react';
@@ -221,7 +222,7 @@ export function TeamSettings({projectId, currentUserRole, currentUserId}: TeamSe
<CardContent>
{isLoading ? (
<div className="flex justify-center py-8">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-neutral-200 border-t-neutral-900" />
<IconSpinner />
</div>
) : members.length === 0 ? (
<div className="py-8 text-center text-sm text-neutral-500">No members found</div>
@@ -12,6 +12,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
IconSpinner,
Input,
Label,
} from '@plunk/ui';
@@ -205,25 +206,9 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
<div className="flex-1 overflow-y-auto space-y-3">
{isLoading && (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading templates...</p>
</div>
<IconSpinner />
</div>
)}
{!isLoading && data?.data.length === 0 && (
<EmptyState
icon={FileText}
+3 -12
View File
@@ -16,6 +16,7 @@ import {
FormItem,
FormLabel,
FormMessage,
IconSpinner,
Input,
} from '@plunk/ui';
import {AnimatePresence, motion} from 'framer-motion';
@@ -33,16 +34,6 @@ import {useProjects} from '../../lib/hooks/useProject';
import {useUser} from '../../lib/hooks/useUser';
import {network} from '../../lib/network';
const Spinner = () => (
<svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
export default function Login() {
const {mutate: userMutate} = useUser();
@@ -296,7 +287,7 @@ export default function Login() {
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<Spinner />
<IconSpinner size="sm" />
Signing in...
</>
) : (
@@ -352,7 +343,7 @@ export default function Login() {
<Button className="w-full" type="submit" disabled={resetStatus === 'loading'}>
{resetStatus === 'loading' ? (
<>
<Spinner />
<IconSpinner size="sm" />
Sending...
</>
) : (
+2 -11
View File
@@ -10,6 +10,7 @@ import {
FormItem,
FormLabel,
FormMessage,
IconSpinner,
Input,
} from '@plunk/ui';
import {AnimatePresence, motion} from 'framer-motion';
@@ -29,16 +30,6 @@ const dotGrid = {
backgroundSize: '20px 20px',
};
const Spinner = () => (
<svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
const Wordmark = () => (
<div className="flex items-center justify-center gap-2.5">
@@ -202,7 +193,7 @@ export default function ResetPassword() {
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<Spinner />
<IconSpinner size="sm" />
Resetting...
</>
) : (
+2 -11
View File
@@ -10,6 +10,7 @@ import {
FormItem,
FormLabel,
FormMessage,
IconSpinner,
Input,
} from '@plunk/ui';
import {AnimatePresence, motion} from 'framer-motion';
@@ -27,16 +28,6 @@ import {useProjects} from '../../lib/hooks/useProject';
import {useUser} from '../../lib/hooks/useUser';
import {network} from '../../lib/network';
const Spinner = () => (
<svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
export default function Signup() {
const {mutate: userMutate} = useUser();
@@ -226,7 +217,7 @@ export default function Signup() {
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<Spinner />
<IconSpinner size="sm" />
Creating account...
</>
) : (
+2 -12
View File
@@ -1,5 +1,5 @@
import {AuthenticationSchemas} from '@plunk/shared';
import {Button, Card, CardContent} from '@plunk/ui';
import {Button, Card, CardContent, IconSpinner} from '@plunk/ui';
import {AnimatePresence, motion} from 'framer-motion';
import {NextSeo} from 'next-seo';
import Image from 'next/image';
@@ -15,16 +15,6 @@ const dotGrid = {
backgroundSize: '20px 20px',
};
const Spinner = () => (
<svg className="h-6 w-6 animate-spin text-neutral-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
export default function VerifyEmail() {
const router = useRouter();
@@ -219,7 +209,7 @@ export default function VerifyEmail() {
className="flex flex-col items-center gap-4"
>
<div className="h-12 w-12 rounded-full bg-neutral-100 flex items-center justify-center">
<Spinner />
<IconSpinner size="sm" />
</div>
<div className="flex flex-col gap-1.5">
<h1 className="text-xl font-bold tracking-tight">Verifying...</h1>
+2 -4
View File
@@ -26,6 +26,7 @@ import {
SelectItemWithDescription,
SelectTrigger,
SelectValue,
IconSpinner,
StickySaveBar,
} from '@plunk/ui';
import type {Campaign, Segment} from '@plunk/db';
@@ -308,10 +309,7 @@ export default function CampaignDetailsPage() {
return (
<DashboardLayout>
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="h-8 w-8 animate-spin mx-auto border-4 border-neutral-200 border-t-neutral-900 rounded-full" />
<p className="mt-2 text-sm text-neutral-500">Loading campaign...</p>
</div>
<IconSpinner />
</div>
</DashboardLayout>
);
+4 -15
View File
@@ -6,6 +6,7 @@ import {
CardDescription,
CardHeader,
CardTitle,
IconSpinner,
Input,
Label,
Select,
@@ -166,21 +167,9 @@ export default function CreateCampaignPage() {
<NextSeo title="Create Campaign" />
<DashboardLayout>
{loadingTemplate && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900 mb-3"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center">
<div className="bg-white rounded-lg p-5 flex items-center gap-3">
<IconSpinner />
<p className="text-sm text-neutral-700">Loading template...</p>
</div>
</div>
+3 -2
View File
@@ -5,13 +5,14 @@ import {
CardHeader,
CardTitle,
ConfirmDialog,
IconSpinner,
Input,
Label,
Switch,
} from '@plunk/ui';
import type {Contact} from '@plunk/db';
import {AnimatePresence, motion} from 'framer-motion';
import {ArrowLeft, Check, Copy, Database, ExternalLink, Loader2, Save, Settings, Trash2} from 'lucide-react';
import {ArrowLeft, Check, Copy, Database, ExternalLink, Save, Settings, Trash2} from 'lucide-react';
import Link from 'next/link';
import {NextSeo} from 'next-seo';
import {useRouter} from 'next/router';
@@ -90,7 +91,7 @@ export default function ContactDetailPage() {
return (
<DashboardLayout>
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-neutral-400" />
<IconSpinner />
</div>
</DashboardLayout>
);
+2 -16
View File
@@ -12,6 +12,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
IconSpinner,
Input,
Label,
Switch,
@@ -265,22 +266,7 @@ export default function ContactsPage() {
<CardContent>
{isLoading && contacts.length === 0 ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading contacts...</p>
</div>
<IconSpinner />
</div>
) : contacts.length === 0 ? (
<EmptyState
+3 -27
View File
@@ -1,4 +1,4 @@
import {Button, Card, CardContent} from '@plunk/ui';
import {Button, Card, CardContent, IconSpinner} from '@plunk/ui';
import {createTranslator, type Translator} from '@plunk/shared';
import {AnimatePresence, motion} from 'framer-motion';
import {useRouter} from 'next/router';
@@ -82,19 +82,7 @@ export default function Manage() {
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center gap-4">
<svg
className="h-8 w-8 animate-spin text-neutral-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner />
<p className="text-sm text-neutral-500">Loading...</p>
</div>
</CardContent>
@@ -111,19 +99,7 @@ export default function Manage() {
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center gap-4">
<svg
className="h-8 w-8 animate-spin text-neutral-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner />
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
</div>
</CardContent>
+2 -20
View File
@@ -12,6 +12,7 @@ import {
FormItem,
FormLabel,
FormMessage,
IconSpinner,
Input,
} from '@plunk/ui';
import {AnimatePresence, motion} from 'framer-motion';
@@ -129,26 +130,7 @@ export default function CreateProject() {
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<svg
className="h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner size="sm" className="mr-2" />
Creating...
</>
) : (
+2 -16
View File
@@ -6,6 +6,7 @@ import {
CardHeader,
CardTitle,
ConfirmDialog,
IconSpinner,
Input,
Label,
} from '@plunk/ui';
@@ -178,22 +179,7 @@ export default function SegmentDetailPage() {
return (
<DashboardLayout>
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading segment...</p>
</div>
<IconSpinner />
</div>
</DashboardLayout>
);
+2 -16
View File
@@ -8,6 +8,7 @@ import {
CardHeader,
CardTitle,
ConfirmDialog,
IconSpinner,
} from '@plunk/ui';
import type {Segment} from '@plunk/db';
import type {FilterCondition} from '@plunk/types';
@@ -105,22 +106,7 @@ export default function SegmentsPage() {
{/* Segments Grid */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading segments...</p>
</div>
<IconSpinner />
</div>
) : segments?.length === 0 ? (
<Card>
+4 -40
View File
@@ -1,4 +1,4 @@
import {Button, Card, CardContent} from '@plunk/ui';
import {Button, Card, CardContent, IconSpinner} from '@plunk/ui';
import {createTranslator, type Translator} from '@plunk/shared';
import {AnimatePresence, motion} from 'framer-motion';
import {useRouter} from 'next/router';
@@ -72,19 +72,7 @@ export default function Subscribe() {
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center gap-4">
<svg
className="h-8 w-8 animate-spin text-neutral-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner />
<p className="text-sm text-neutral-500">Loading...</p>
</div>
</CardContent>
@@ -101,19 +89,7 @@ export default function Subscribe() {
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center gap-4">
<svg
className="h-8 w-8 animate-spin text-neutral-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner />
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
</div>
</CardContent>
@@ -219,19 +195,7 @@ export default function Subscribe() {
<Button onClick={() => void handleSubscribe()} className="w-full" disabled={subscribing}>
{subscribing ? (
<div className="flex items-center gap-2">
<svg
className="h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner size="sm" />
<span>{translator.t('pages.subscribe.buttonLoading')}</span>
</div>
) : (
+2 -16
View File
@@ -6,6 +6,7 @@ import {
CardHeader,
CardTitle,
ConfirmDialog,
IconSpinner,
Input,
Label,
StickySaveBar,
@@ -117,22 +118,7 @@ export default function TemplateEditorPage() {
return (
<DashboardLayout>
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading template...</p>
</div>
<IconSpinner />
</div>
</DashboardLayout>
);
+2 -16
View File
@@ -7,6 +7,7 @@ import {
CardHeader,
CardTitle,
ConfirmDialog,
IconSpinner,
Input,
} from '@plunk/ui';
import type {Template} from '@plunk/db';
@@ -165,22 +166,7 @@ export default function TemplatesPage() {
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading templates...</p>
</div>
<IconSpinner />
</div>
</CardContent>
</Card>
+4 -40
View File
@@ -1,4 +1,4 @@
import {Button, Card, CardContent} from '@plunk/ui';
import {Button, Card, CardContent, IconSpinner} from '@plunk/ui';
import {createTranslator, type Translator} from '@plunk/shared';
import {AnimatePresence, motion} from 'framer-motion';
import {useRouter} from 'next/router';
@@ -72,19 +72,7 @@ export default function Unsubscribe() {
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center gap-4">
<svg
className="h-8 w-8 animate-spin text-neutral-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner />
<p className="text-sm text-neutral-500">Loading...</p>
</div>
</CardContent>
@@ -101,19 +89,7 @@ export default function Unsubscribe() {
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center gap-4">
<svg
className="h-8 w-8 animate-spin text-neutral-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner />
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
</div>
</CardContent>
@@ -236,19 +212,7 @@ export default function Unsubscribe() {
>
{unsubscribing ? (
<div className="flex items-center gap-2">
<svg
className="h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner size="sm" />
<span>{translator.t('pages.unsubscribe.buttonLoading')}</span>
</div>
) : (
+4 -42
View File
@@ -30,6 +30,7 @@ import {
CommandGroup,
CommandItem,
CommandList,
IconSpinner,
Switch,
} from '@plunk/ui';
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
@@ -406,22 +407,7 @@ export default function WorkflowEditorPage() {
return (
<DashboardLayout>
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading workflow...</p>
</div>
<IconSpinner />
</div>
</DashboardLayout>
);
@@ -1463,19 +1449,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
</Label>
{loadingFields ? (
<div className="flex items-center gap-2 px-3 py-2 border border-neutral-200 rounded-lg text-sm text-neutral-500 mt-1.5">
<svg
className="h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner size="sm" />
Loading fields...
</div>
) : availableFields.length > 0 ? (
@@ -2520,19 +2494,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
<Label htmlFor="editConditionField">Field to Check *</Label>
{loadingFields ? (
<div className="flex items-center gap-2 px-3 py-2 border border-neutral-200 rounded-lg text-sm text-neutral-500 mt-1.5">
<svg
className="h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<IconSpinner size="sm" />
Loading fields...
</div>
) : availableFields.length > 0 ? (
+2 -16
View File
@@ -16,6 +16,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
IconSpinner,
Input,
Label,
} from '@plunk/ui';
@@ -137,22 +138,7 @@ export default function WorkflowsPage() {
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<svg
className="h-8 w-8 animate-spin mx-auto text-neutral-900"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-2 text-sm text-neutral-500">Loading workflows...</p>
</div>
<IconSpinner />
</div>
</CardContent>
</Card>