ui: replace custom loading spinners with IconSpinner component across multiple files
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import {Button} from '@plunk/ui';
|
import {Button, IconSpinner} from '@plunk/ui';
|
||||||
import type {Activity, CursorPaginatedResponse} from '@plunk/types';
|
import type {Activity, CursorPaginatedResponse} from '@plunk/types';
|
||||||
import {network} from '../lib/network';
|
import {network} from '../lib/network';
|
||||||
import {ActivityItem} from './ActivityItem';
|
import {ActivityItem} from './ActivityItem';
|
||||||
import {EmptyState} from './EmptyState';
|
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';
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
|
|
||||||
export interface ActivityFeedProps {
|
export interface ActivityFeedProps {
|
||||||
@@ -169,7 +169,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-neutral-400" />
|
<IconSpinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
|
|||||||
<Button onClick={loadMore} variant="outline" disabled={isLoadingMore}>
|
<Button onClick={loadMore} variant="outline" disabled={isLoadingMore}>
|
||||||
{isLoadingMore ? (
|
{isLoadingMore ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
<IconSpinner size="sm" className="mr-2" />
|
||||||
Loading...
|
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 {AlertCircle, Coins, TrendingUp} from 'lucide-react';
|
||||||
import {useBillingConsumption} from '../lib/hooks/useBillingConsumption';
|
import {useBillingConsumption} from '../lib/hooks/useBillingConsumption';
|
||||||
import {useConfig} from '../lib/hooks/useConfig';
|
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>
|
<CardDescription>View your current month email consumption and costs</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-neutral-500">Loading...</p>
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<IconSpinner size="sm" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {AlertCircle, Download, ExternalLink, FileText} from 'lucide-react';
|
||||||
import {useBillingInvoices} from '../lib/hooks/useBillingInvoices';
|
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>
|
<CardDescription>View and download your billing history</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-neutral-500">Loading...</p>
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<IconSpinner size="sm" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
|
IconSpinner,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -146,7 +147,9 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
|
|||||||
<CardDescription>Set monthly limits for each email category</CardDescription>
|
<CardDescription>Set monthly limits for each email category</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-neutral-500">Loading...</p>
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<IconSpinner size="sm" />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
|
IconSpinner,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Campaign} from '@plunk/db';
|
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">
|
<div className="flex-1 overflow-y-auto space-y-3">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
IconSpinner,
|
||||||
} from '@plunk/ui';
|
} 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 {EmptyState} from './EmptyState';
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -345,7 +346,7 @@ export function DataManagementSettings() {
|
|||||||
isDeleting || (!!selectedField && !fieldUsage?.canDelete) || (!!selectedEvent && !eventUsage?.canDelete)
|
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
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
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 {EmptyState} from './EmptyState';
|
||||||
import {useConfig} from '../lib/hooks/useConfig';
|
import {useConfig} from '../lib/hooks/useConfig';
|
||||||
import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains';
|
import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains';
|
||||||
@@ -322,7 +323,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-neutral-400" />
|
<IconSpinner />
|
||||||
</div>
|
</div>
|
||||||
) : !domains || domains.length === 0 ? (
|
) : !domains || domains.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -360,7 +361,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
|||||||
className="min-w-[80px]"
|
className="min-w-[80px]"
|
||||||
>
|
>
|
||||||
{checkingVerification === domain.id ? (
|
{checkingVerification === domain.id ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<IconSpinner size="sm" />
|
||||||
) : (cooldownSeconds[domain.id] ?? 0) > 0 ? (
|
) : (cooldownSeconds[domain.id] ?? 0) > 0 ? (
|
||||||
<span className="text-xs">{cooldownSeconds[domain.id]}s</span>
|
<span className="text-xs">{cooldownSeconds[domain.id]}s</span>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
IconSpinner,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {MembershipSchemas} from '@plunk/shared';
|
import {MembershipSchemas} from '@plunk/shared';
|
||||||
import {MoreVertical, Trash2, UserPlus} from 'lucide-react';
|
import {MoreVertical, Trash2, UserPlus} from 'lucide-react';
|
||||||
@@ -221,7 +222,7 @@ export function TeamSettings({projectId, currentUserRole, currentUserId}: TeamSe
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-8">
|
<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>
|
</div>
|
||||||
) : members.length === 0 ? (
|
) : members.length === 0 ? (
|
||||||
<div className="py-8 text-center text-sm text-neutral-500">No members found</div>
|
<div className="py-8 text-center text-sm text-neutral-500">No members found</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
@@ -205,25 +206,9 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
|||||||
<div className="flex-1 overflow-y-auto space-y-3">
|
<div className="flex-1 overflow-y-auto space-y-3">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && data?.data.length === 0 && (
|
{!isLoading && data?.data.length === 0 && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={FileText}
|
icon={FileText}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
@@ -33,16 +34,6 @@ import {useProjects} from '../../lib/hooks/useProject';
|
|||||||
import {useUser} from '../../lib/hooks/useUser';
|
import {useUser} from '../../lib/hooks/useUser';
|
||||||
import {network} from '../../lib/network';
|
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() {
|
export default function Login() {
|
||||||
const {mutate: userMutate} = useUser();
|
const {mutate: userMutate} = useUser();
|
||||||
@@ -296,7 +287,7 @@ export default function Login() {
|
|||||||
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
||||||
{form.formState.isSubmitting ? (
|
{form.formState.isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Spinner />
|
<IconSpinner size="sm" />
|
||||||
Signing in...
|
Signing in...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -352,7 +343,7 @@ export default function Login() {
|
|||||||
<Button className="w-full" type="submit" disabled={resetStatus === 'loading'}>
|
<Button className="w-full" type="submit" disabled={resetStatus === 'loading'}>
|
||||||
{resetStatus === 'loading' ? (
|
{resetStatus === 'loading' ? (
|
||||||
<>
|
<>
|
||||||
<Spinner />
|
<IconSpinner size="sm" />
|
||||||
Sending...
|
Sending...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
@@ -29,16 +30,6 @@ const dotGrid = {
|
|||||||
backgroundSize: '20px 20px',
|
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 = () => (
|
const Wordmark = () => (
|
||||||
<div className="flex items-center justify-center gap-2.5">
|
<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}>
|
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
||||||
{form.formState.isSubmitting ? (
|
{form.formState.isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Spinner />
|
<IconSpinner size="sm" />
|
||||||
Resetting...
|
Resetting...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
@@ -27,16 +28,6 @@ import {useProjects} from '../../lib/hooks/useProject';
|
|||||||
import {useUser} from '../../lib/hooks/useUser';
|
import {useUser} from '../../lib/hooks/useUser';
|
||||||
import {network} from '../../lib/network';
|
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() {
|
export default function Signup() {
|
||||||
const {mutate: userMutate} = useUser();
|
const {mutate: userMutate} = useUser();
|
||||||
@@ -226,7 +217,7 @@ export default function Signup() {
|
|||||||
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
||||||
{form.formState.isSubmitting ? (
|
{form.formState.isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Spinner />
|
<IconSpinner size="sm" />
|
||||||
Creating account...
|
Creating account...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {AuthenticationSchemas} from '@plunk/shared';
|
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 {AnimatePresence, motion} from 'framer-motion';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -15,16 +15,6 @@ const dotGrid = {
|
|||||||
backgroundSize: '20px 20px',
|
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() {
|
export default function VerifyEmail() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -219,7 +209,7 @@ export default function VerifyEmail() {
|
|||||||
className="flex flex-col items-center gap-4"
|
className="flex flex-col items-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="h-12 w-12 rounded-full bg-neutral-100 flex items-center justify-center">
|
<div className="h-12 w-12 rounded-full bg-neutral-100 flex items-center justify-center">
|
||||||
<Spinner />
|
<IconSpinner size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h1 className="text-xl font-bold tracking-tight">Verifying...</h1>
|
<h1 className="text-xl font-bold tracking-tight">Verifying...</h1>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
SelectItemWithDescription,
|
SelectItemWithDescription,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
|
IconSpinner,
|
||||||
StickySaveBar,
|
StickySaveBar,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Campaign, Segment} from '@plunk/db';
|
import type {Campaign, Segment} from '@plunk/db';
|
||||||
@@ -308,10 +309,7 @@ export default function CampaignDetailsPage() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -166,21 +167,9 @@ export default function CreateCampaignPage() {
|
|||||||
<NextSeo title="Create Campaign" />
|
<NextSeo title="Create Campaign" />
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
{loadingTemplate && (
|
{loadingTemplate && (
|
||||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center">
|
||||||
<div className="bg-white rounded-lg p-6 text-center">
|
<div className="bg-white rounded-lg p-5 flex items-center gap-3">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-700">Loading template...</p>
|
<p className="text-sm text-neutral-700">Loading template...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Switch,
|
Switch,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Contact} from '@plunk/db';
|
import type {Contact} from '@plunk/db';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
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 Link from 'next/link';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
import {useRouter} from 'next/router';
|
import {useRouter} from 'next/router';
|
||||||
@@ -90,7 +91,7 @@ export default function ContactDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-neutral-400" />
|
<IconSpinner />
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -265,22 +266,7 @@ export default function ContactsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading && contacts.length === 0 ? (
|
{isLoading && contacts.length === 0 ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
) : contacts.length === 0 ? (
|
) : contacts.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|||||||
@@ -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 {createTranslator, type Translator} from '@plunk/shared';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
import {useRouter} from 'next/router';
|
import {useRouter} from 'next/router';
|
||||||
@@ -82,19 +82,7 @@ export default function Manage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-500">Loading...</p>
|
<p className="text-sm text-neutral-500">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -111,19 +99,7 @@ export default function Manage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
|
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
@@ -129,26 +130,7 @@ export default function CreateProject() {
|
|||||||
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
||||||
{form.formState.isSubmitting ? (
|
{form.formState.isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<svg
|
<IconSpinner size="sm" className="mr-2" />
|
||||||
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>
|
|
||||||
Creating...
|
Creating...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
@@ -178,22 +179,7 @@ export default function SegmentDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
IconSpinner,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Segment} from '@plunk/db';
|
import type {Segment} from '@plunk/db';
|
||||||
import type {FilterCondition} from '@plunk/types';
|
import type {FilterCondition} from '@plunk/types';
|
||||||
@@ -105,22 +106,7 @@ export default function SegmentsPage() {
|
|||||||
{/* Segments Grid */}
|
{/* Segments Grid */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
) : segments?.length === 0 ? (
|
) : segments?.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -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 {createTranslator, type Translator} from '@plunk/shared';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
import {useRouter} from 'next/router';
|
import {useRouter} from 'next/router';
|
||||||
@@ -72,19 +72,7 @@ export default function Subscribe() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-500">Loading...</p>
|
<p className="text-sm text-neutral-500">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -101,19 +89,7 @@ export default function Subscribe() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
|
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -219,19 +195,7 @@ export default function Subscribe() {
|
|||||||
<Button onClick={() => void handleSubscribe()} className="w-full" disabled={subscribing}>
|
<Button onClick={() => void handleSubscribe()} className="w-full" disabled={subscribing}>
|
||||||
{subscribing ? (
|
{subscribing ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg
|
<IconSpinner size="sm" />
|
||||||
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>
|
|
||||||
<span>{translator.t('pages.subscribe.buttonLoading')}</span>
|
<span>{translator.t('pages.subscribe.buttonLoading')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
StickySaveBar,
|
StickySaveBar,
|
||||||
@@ -117,22 +118,7 @@ export default function TemplateEditorPage() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Template} from '@plunk/db';
|
import type {Template} from '@plunk/db';
|
||||||
@@ -165,22 +166,7 @@ export default function TemplatesPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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 {createTranslator, type Translator} from '@plunk/shared';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
import {useRouter} from 'next/router';
|
import {useRouter} from 'next/router';
|
||||||
@@ -72,19 +72,7 @@ export default function Unsubscribe() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-500">Loading...</p>
|
<p className="text-sm text-neutral-500">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -101,19 +89,7 @@ export default function Unsubscribe() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<svg
|
<IconSpinner />
|
||||||
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>
|
|
||||||
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
|
<p className="text-sm text-neutral-500">{translator.t('pages.common.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -236,19 +212,7 @@ export default function Unsubscribe() {
|
|||||||
>
|
>
|
||||||
{unsubscribing ? (
|
{unsubscribing ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg
|
<IconSpinner size="sm" />
|
||||||
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>
|
|
||||||
<span>{translator.t('pages.unsubscribe.buttonLoading')}</span>
|
<span>{translator.t('pages.unsubscribe.buttonLoading')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
|
IconSpinner,
|
||||||
Switch,
|
Switch,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
|
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
|
||||||
@@ -406,22 +407,7 @@ export default function WorkflowEditorPage() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
@@ -1463,19 +1449,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
|||||||
</Label>
|
</Label>
|
||||||
{loadingFields ? (
|
{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">
|
<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
|
<IconSpinner size="sm" />
|
||||||
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>
|
|
||||||
Loading fields...
|
Loading fields...
|
||||||
</div>
|
</div>
|
||||||
) : availableFields.length > 0 ? (
|
) : availableFields.length > 0 ? (
|
||||||
@@ -2520,19 +2494,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
<Label htmlFor="editConditionField">Field to Check *</Label>
|
<Label htmlFor="editConditionField">Field to Check *</Label>
|
||||||
{loadingFields ? (
|
{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">
|
<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
|
<IconSpinner size="sm" />
|
||||||
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>
|
|
||||||
Loading fields...
|
Loading fields...
|
||||||
</div>
|
</div>
|
||||||
) : availableFields.length > 0 ? (
|
) : availableFields.length > 0 ? (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
@@ -137,22 +138,7 @@ export default function WorkflowsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<IconSpinner />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,74 +1,25 @@
|
|||||||
import {cn} from '../../lib';
|
import {cn} from '../../lib';
|
||||||
|
|
||||||
export interface IconSpinnerProps {
|
export interface IconSpinnerProps {
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeConfig = {
|
const sizeConfig = {
|
||||||
sm: {
|
sm: 'w-4 h-4 border-[1.5px]',
|
||||||
container: 'w-4 h-4',
|
md: 'w-5 h-5 border-[1.5px]',
|
||||||
border: 'border-2',
|
lg: 'w-7 h-7 border-2',
|
||||||
innerInset: 'inset-0.5',
|
|
||||||
dot: 'w-1 h-1',
|
|
||||||
},
|
|
||||||
md: {
|
|
||||||
container: 'w-8 h-8',
|
|
||||||
border: 'border-[2.5px]',
|
|
||||||
innerInset: 'inset-1',
|
|
||||||
dot: 'w-1.5 h-1.5',
|
|
||||||
},
|
|
||||||
lg: {
|
|
||||||
container: 'w-12 h-12',
|
|
||||||
border: 'border-[3px]',
|
|
||||||
innerInset: 'inset-1.5',
|
|
||||||
dot: 'w-2 h-2',
|
|
||||||
},
|
|
||||||
xl: {
|
|
||||||
container: 'w-16 h-16',
|
|
||||||
border: 'border-[3px]',
|
|
||||||
innerInset: 'inset-2',
|
|
||||||
dot: 'w-2.5 h-2.5',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Animated spinner icon component with dual rotating rings
|
|
||||||
*/
|
|
||||||
export function IconSpinner({size = 'md', className}: IconSpinnerProps) {
|
export function IconSpinner({size = 'md', className}: IconSpinnerProps) {
|
||||||
const config = sizeConfig[size];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative inline-block', config.container, className)}>
|
<div
|
||||||
{/* Outer glow effect */}
|
className={cn(
|
||||||
<div className="absolute inset-0 rounded-full bg-neutral-900/10 blur-sm animate-pulse" />
|
'rounded-full border-neutral-200 border-t-neutral-800 animate-spin flex-shrink-0',
|
||||||
|
sizeConfig[size],
|
||||||
{/* Base circle */}
|
className,
|
||||||
<div className={cn('absolute inset-0 rounded-full border-neutral-100', config.border)} />
|
)}
|
||||||
|
style={{animationDuration: '0.65s'}}
|
||||||
{/* Primary animated ring */}
|
/>
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute inset-0 rounded-full border-transparent border-t-neutral-900 border-r-neutral-700 animate-spin',
|
|
||||||
config.border,
|
|
||||||
)}
|
|
||||||
style={{animationDuration: '0.8s'}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Secondary animated ring (counter-rotating) */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute rounded-full border-transparent border-b-neutral-800 border-l-neutral-600 animate-spin',
|
|
||||||
config.border,
|
|
||||||
config.innerInset,
|
|
||||||
)}
|
|
||||||
style={{animationDuration: '1.2s', animationDirection: 'reverse'}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Center dot */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className={cn('bg-neutral-900 rounded-full animate-pulse', config.dot)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {useRouter} from 'next/router';
|
import {useRouter} from 'next/router';
|
||||||
|
import {IconSpinner} from './IconSpinner';
|
||||||
|
|
||||||
export interface LoaderProps {
|
export interface LoaderProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
showLogo?: boolean;
|
showLogo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function Loader({message, showLogo = true}: LoaderProps) {
|
||||||
* Full-screen loader component with animated logo and spinner
|
|
||||||
*/
|
|
||||||
export function Loader({message = 'Loading...', showLogo = true}: LoaderProps) {
|
|
||||||
const [showClearCache, setShowClearCache] = useState(false);
|
const [showClearCache, setShowClearCache] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => setShowClearCache(true), 5000);
|
||||||
setShowClearCache(true);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -26,60 +21,28 @@ export function Loader({message = 'Loading...', showLogo = true}: LoaderProps) {
|
|||||||
document.cookie.split(';').forEach(c => {
|
document.cookie.split(';').forEach(c => {
|
||||||
document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
|
document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
|
||||||
void router.push('/auth/login');
|
void router.push('/auth/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex items-center justify-center bg-white">
|
<div className="h-screen flex items-center justify-center bg-white">
|
||||||
<div className="text-center animate-in fade-in duration-500">
|
<div className="flex flex-col items-center gap-6 animate-in fade-in duration-500">
|
||||||
{showLogo && (
|
{showLogo && (
|
||||||
<div className="mb-8 animate-in zoom-in-95 duration-700">
|
<Image src="/assets/logo.png" alt="Plunk" width={40} height={40} className="rounded-lg" priority />
|
||||||
<div className="relative w-20 h-20 mx-auto p-4 bg-white rounded-2xl shadow-sm border border-neutral-200/50">
|
|
||||||
<Image src="/assets/logo.png" alt="Plunk" width={48} height={48} className="rounded-lg" priority />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Animated spinner */}
|
<IconSpinner />
|
||||||
<div className="relative w-16 h-16 mx-auto mb-6">
|
|
||||||
{/* Outer glow effect */}
|
|
||||||
<div className="absolute inset-0 rounded-full bg-neutral-900/10 blur-xl animate-pulse" />
|
|
||||||
|
|
||||||
{/* Base circle */}
|
{message && <p className="text-xs text-neutral-400">{message}</p>}
|
||||||
<div className="absolute inset-0 rounded-full border-[3px] border-neutral-100" />
|
|
||||||
|
|
||||||
{/* Primary animated ring */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 rounded-full border-[3px] border-transparent border-t-neutral-900 border-r-neutral-700 animate-spin"
|
|
||||||
style={{animationDuration: '0.8s'}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Secondary animated ring (counter-rotating) */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-2 rounded-full border-[3px] border-transparent border-b-neutral-800 border-l-neutral-600 animate-spin"
|
|
||||||
style={{animationDuration: '1.2s', animationDirection: 'reverse'}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Center dot */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="w-2.5 h-2.5 bg-neutral-900 rounded-full animate-pulse shadow-lg" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Loading message */}
|
|
||||||
{message && <p className="text-sm font-medium text-neutral-700 animate-in fade-in duration-1000">{message}</p>}
|
|
||||||
|
|
||||||
{/* Clear cache button - appears after 5 seconds */}
|
|
||||||
{showClearCache && (
|
{showClearCache && (
|
||||||
<div className="mt-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
<div className="animate-in fade-in slide-in-from-bottom-1 duration-500 text-center">
|
||||||
<p className="text-xs text-neutral-500 mb-3">Taking too long?</p>
|
<p className="text-xs text-neutral-400 mb-2">Taking too long?</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleClearCache}
|
onClick={handleClearCache}
|
||||||
className="px-4 py-2 text-sm font-medium text-neutral-700 bg-neutral-100 hover:bg-neutral-200 rounded-lg transition-colors duration-200"
|
className="text-xs text-neutral-500 hover:text-neutral-800 underline underline-offset-2 transition-colors duration-150"
|
||||||
>
|
>
|
||||||
Clear cache and reload
|
Clear cache and reload
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user