fix: Consistency across buttons and labels
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
import {Button, IconSpinner} from '@plunk/ui';
|
import {Button, EmptyState, 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 {Activity as ActivityIcon} from 'lucide-react';
|
import {Activity as ActivityIcon} from 'lucide-react';
|
||||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'event.triggered':
|
case 'event.triggered':
|
||||||
return {
|
return {
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
color: 'text-blue-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-blue-100',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.eventName === 'string' ? metadata.eventName : undefined) || 'Event triggered',
|
title: (typeof metadata.eventName === 'string' ? metadata.eventName : undefined) || 'Event triggered',
|
||||||
description: undefined,
|
description: undefined,
|
||||||
badge: {
|
badge: {
|
||||||
@@ -150,8 +150,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.sent':
|
case 'email.sent':
|
||||||
return {
|
return {
|
||||||
icon: Send,
|
icon: Send,
|
||||||
color: 'text-green-600',
|
color: 'text-green-700',
|
||||||
bgColor: 'bg-green-100',
|
bgColor: 'bg-green-50',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email sent',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email sent',
|
||||||
description: metadata.campaignName
|
description: metadata.campaignName
|
||||||
? `Campaign: ${String(metadata.campaignName)}`
|
? `Campaign: ${String(metadata.campaignName)}`
|
||||||
@@ -169,8 +169,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.delivered':
|
case 'email.delivered':
|
||||||
return {
|
return {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: 'text-green-600',
|
color: 'text-green-700',
|
||||||
bgColor: 'bg-green-100',
|
bgColor: 'bg-green-50',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email delivered',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email delivered',
|
||||||
description: metadata.campaignName
|
description: metadata.campaignName
|
||||||
? `Campaign: ${String(metadata.campaignName)}`
|
? `Campaign: ${String(metadata.campaignName)}`
|
||||||
@@ -186,8 +186,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.received':
|
case 'email.received':
|
||||||
return {
|
return {
|
||||||
icon: Inbox,
|
icon: Inbox,
|
||||||
color: 'text-blue-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-blue-100',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email received',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email received',
|
||||||
description: typeof metadata.from === 'string' ? `From: ${metadata.from}` : 'Inbound email',
|
description: typeof metadata.from === 'string' ? `From: ${metadata.from}` : 'Inbound email',
|
||||||
badge: {
|
badge: {
|
||||||
@@ -199,8 +199,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.opened':
|
case 'email.opened':
|
||||||
return {
|
return {
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
color: 'text-purple-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-purple-100',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email opened',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email opened',
|
||||||
description:
|
description:
|
||||||
typeof metadata.totalOpens === 'number' && metadata.totalOpens > 1
|
typeof metadata.totalOpens === 'number' && metadata.totalOpens > 1
|
||||||
@@ -219,8 +219,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.clicked':
|
case 'email.clicked':
|
||||||
return {
|
return {
|
||||||
icon: MousePointerClick,
|
icon: MousePointerClick,
|
||||||
color: 'text-orange-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-orange-100',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email clicked',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email clicked',
|
||||||
description:
|
description:
|
||||||
typeof metadata.totalClicks === 'number' && metadata.totalClicks > 1
|
typeof metadata.totalClicks === 'number' && metadata.totalClicks > 1
|
||||||
@@ -239,8 +239,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.bounced':
|
case 'email.bounced':
|
||||||
return {
|
return {
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
color: 'text-red-600',
|
color: 'text-red-700',
|
||||||
bgColor: 'bg-red-100',
|
bgColor: 'bg-red-50',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email bounced',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email bounced',
|
||||||
description: (typeof metadata.error === 'string' ? metadata.error : undefined) || 'Email failed to deliver',
|
description: (typeof metadata.error === 'string' ? metadata.error : undefined) || 'Email failed to deliver',
|
||||||
badge: {
|
badge: {
|
||||||
@@ -252,8 +252,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'email.complaint':
|
case 'email.complaint':
|
||||||
return {
|
return {
|
||||||
icon: ShieldAlert,
|
icon: ShieldAlert,
|
||||||
color: 'text-red-600',
|
color: 'text-red-700',
|
||||||
bgColor: 'bg-red-100',
|
bgColor: 'bg-red-50',
|
||||||
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Spam complaint',
|
title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Spam complaint',
|
||||||
description: metadata.campaignName
|
description: metadata.campaignName
|
||||||
? `Campaign: ${String(metadata.campaignName)}`
|
? `Campaign: ${String(metadata.campaignName)}`
|
||||||
@@ -269,8 +269,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'workflow.started':
|
case 'workflow.started':
|
||||||
return {
|
return {
|
||||||
icon: Workflow,
|
icon: Workflow,
|
||||||
color: 'text-indigo-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-indigo-100',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.workflowName === 'string' ? metadata.workflowName : undefined) || 'Workflow started',
|
title: (typeof metadata.workflowName === 'string' ? metadata.workflowName : undefined) || 'Workflow started',
|
||||||
description: `Status: ${String(metadata.status || 'unknown')}`,
|
description: `Status: ${String(metadata.status || 'unknown')}`,
|
||||||
badge: {
|
badge: {
|
||||||
@@ -282,8 +282,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'workflow.completed':
|
case 'workflow.completed':
|
||||||
return {
|
return {
|
||||||
icon: CheckCheck,
|
icon: CheckCheck,
|
||||||
color: 'text-green-600',
|
color: 'text-green-700',
|
||||||
bgColor: 'bg-green-100',
|
bgColor: 'bg-green-50',
|
||||||
title: (typeof metadata.workflowName === 'string' ? metadata.workflowName : undefined) || 'Workflow completed',
|
title: (typeof metadata.workflowName === 'string' ? metadata.workflowName : undefined) || 'Workflow completed',
|
||||||
description: metadata.exitReason
|
description: metadata.exitReason
|
||||||
? `Exit: ${String(metadata.exitReason)}`
|
? `Exit: ${String(metadata.exitReason)}`
|
||||||
@@ -297,8 +297,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'campaign.scheduled':
|
case 'campaign.scheduled':
|
||||||
return {
|
return {
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
color: 'text-blue-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-blue-50',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.campaignName === 'string' ? metadata.campaignName : undefined) || 'Campaign scheduled',
|
title: (typeof metadata.campaignName === 'string' ? metadata.campaignName : undefined) || 'Campaign scheduled',
|
||||||
description: metadata.subject
|
description: metadata.subject
|
||||||
? `${String(metadata.subject)}${metadata.totalRecipients ? ` • ${metadata.totalRecipients} recipients` : ''}`
|
? `${String(metadata.subject)}${metadata.totalRecipients ? ` • ${metadata.totalRecipients} recipients` : ''}`
|
||||||
@@ -314,8 +314,8 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
|||||||
case 'workflow.email.scheduled':
|
case 'workflow.email.scheduled':
|
||||||
return {
|
return {
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
color: 'text-indigo-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-indigo-50',
|
bgColor: 'bg-neutral-100',
|
||||||
title: (typeof metadata.stepName === 'string' ? metadata.stepName : undefined) || 'Workflow email scheduled',
|
title: (typeof metadata.stepName === 'string' ? metadata.stepName : undefined) || 'Workflow email scheduled',
|
||||||
description: metadata.workflowName
|
description: metadata.workflowName
|
||||||
? `Workflow: ${String(metadata.workflowName)}${metadata.subject ? ` • ${String(metadata.subject)}` : ''}`
|
? `Workflow: ${String(metadata.workflowName)}${metadata.subject ? ` • ${String(metadata.subject)}` : ''}`
|
||||||
@@ -353,7 +353,7 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f
|
|||||||
<div className={`flex items-start gap-4 ${isUpcoming ? 'opacity-80' : ''}`}>
|
<div className={`flex items-start gap-4 ${isUpcoming ? 'opacity-80' : ''}`}>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div
|
<div
|
||||||
className={`h-10 w-10 rounded-lg ${config.bgColor} flex items-center justify-center flex-shrink-0 ${isUpcoming ? 'ring-2 ring-offset-2 ring-blue-200' : ''}`}
|
className={`h-10 w-10 rounded-lg ${config.bgColor} flex items-center justify-center flex-shrink-0 ${isUpcoming ? 'ring-2 ring-offset-2 ring-neutral-300' : ''}`}
|
||||||
>
|
>
|
||||||
<Icon className={`h-5 w-5 ${config.color}`} />
|
<Icon className={`h-5 w-5 ${config.color}`} />
|
||||||
</div>
|
</div>
|
||||||
@@ -410,7 +410,7 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`text-xs flex-shrink-0 whitespace-nowrap ${isUpcoming ? 'text-blue-600 font-medium' : 'text-neutral-400'}`}
|
className={`text-xs flex-shrink-0 whitespace-nowrap ${isUpcoming ? 'text-neutral-700 font-medium' : 'text-neutral-400'}`}
|
||||||
title={timestamp.toLocaleString()}
|
title={timestamp.toLocaleString()}
|
||||||
>
|
>
|
||||||
{relativeTime}
|
{relativeTime}
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt
|
|||||||
{/* Total Usage */}
|
{/* Total Usage */}
|
||||||
<div className="border border-neutral-200 rounded-lg p-6">
|
<div className="border border-neutral-200 rounded-lg p-6">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div className="p-2 bg-blue-50 rounded-lg">
|
<div className="p-2 bg-neutral-100 rounded-lg">
|
||||||
<TrendingUp className="h-5 w-5 text-blue-600" />
|
<TrendingUp className="h-5 w-5 text-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-neutral-500">Total Emails Sent</p>
|
<p className="text-sm text-neutral-500">Total Emails Sent</p>
|
||||||
@@ -123,19 +123,19 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt
|
|||||||
|
|
||||||
{/* Account Credits */}
|
{/* Account Credits */}
|
||||||
{consumptionData.credits && consumptionData.credits.hasCredits && (
|
{consumptionData.credits && consumptionData.credits.hasCredits && (
|
||||||
<div className="border border-green-200 rounded-lg p-6 bg-green-50">
|
<div className="border border-neutral-200 rounded-lg p-6">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div className="p-2 bg-white rounded-lg shadow-sm border border-green-200">
|
<div className="p-2 bg-neutral-100 rounded-lg">
|
||||||
<Coins className="h-5 w-5 text-green-600" />
|
<Coins className="h-5 w-5 text-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-green-700">Account Credits</p>
|
<p className="text-sm text-neutral-500">Account Credits</p>
|
||||||
<p className="text-2xl font-bold text-green-900">
|
<p className="text-2xl font-bold text-neutral-900">
|
||||||
{formatCurrency(consumptionData.credits.creditAmount, consumptionData.credits.currency)}
|
{formatCurrency(consumptionData.credits.creditAmount, consumptionData.credits.currency)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-green-700 mt-2">
|
<p className="text-xs text-neutral-500 mt-2">
|
||||||
Credits will be automatically applied to your upcoming invoices
|
Credits will be automatically applied to your upcoming invoices
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,16 +90,16 @@ export function BillingInvoices({projectId, hasSubscription, onManageBilling}: B
|
|||||||
const getStatusBadge = (status: string, paid: boolean) => {
|
const getStatusBadge = (status: string, paid: boolean) => {
|
||||||
// Stripe invoices have status 'paid' when paid, or paid boolean is true
|
// Stripe invoices have status 'paid' when paid, or paid boolean is true
|
||||||
if (paid || status === 'paid') {
|
if (paid || status === 'paid') {
|
||||||
return <Badge variant="green">Paid</Badge>;
|
return <Badge variant="success">Paid</Badge>;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'open':
|
case 'open':
|
||||||
return <Badge variant="orange">Unpaid</Badge>;
|
return <Badge variant="warning">Unpaid</Badge>;
|
||||||
case 'draft':
|
case 'draft':
|
||||||
return <Badge variant="neutral">Draft</Badge>;
|
return <Badge variant="neutral">Draft</Badge>;
|
||||||
case 'uncollectible':
|
case 'uncollectible':
|
||||||
return <Badge variant="red">Uncollectible</Badge>;
|
return <Badge variant="destructive">Uncollectible</Badge>;
|
||||||
case 'void':
|
case 'void':
|
||||||
return <Badge variant="neutral">Void</Badge>;
|
return <Badge variant="neutral">Void</Badge>;
|
||||||
default:
|
default:
|
||||||
@@ -169,7 +169,7 @@ export function BillingInvoices({projectId, hasSubscription, onManageBilling}: B
|
|||||||
<span className="font-medium">Total:</span>{' '}
|
<span className="font-medium">Total:</span>{' '}
|
||||||
{formatCurrency(invoice.total, invoice.currency)}
|
{formatCurrency(invoice.total, invoice.currency)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-orange-600 font-medium">
|
<p className="text-amber-700 font-medium">
|
||||||
<span className="font-medium">Amount Due:</span>{' '}
|
<span className="font-medium">Amount Due:</span>{' '}
|
||||||
{formatCurrency(invoice.amountDue, invoice.currency)}
|
{formatCurrency(invoice.amountDue, invoice.currency)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -384,15 +384,15 @@ interface UsageDisplayProps {
|
|||||||
|
|
||||||
const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: UsageDisplayProps) {
|
const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: UsageDisplayProps) {
|
||||||
const statusColor = useMemo(() => {
|
const statusColor = useMemo(() => {
|
||||||
if (usage.isBlocked) return 'text-red-600';
|
if (usage.isBlocked) return 'text-red-700';
|
||||||
if (usage.isWarning) return 'text-orange-600';
|
if (usage.isWarning) return 'text-amber-700';
|
||||||
return 'text-green-600';
|
return 'text-neutral-600';
|
||||||
}, [usage.isBlocked, usage.isWarning]);
|
}, [usage.isBlocked, usage.isWarning]);
|
||||||
|
|
||||||
const progressColor = useMemo(() => {
|
const progressColor = useMemo(() => {
|
||||||
if (usage.isBlocked) return 'bg-red-600';
|
if (usage.isBlocked) return 'bg-red-600';
|
||||||
if (usage.isWarning) return 'bg-orange-500';
|
if (usage.isWarning) return 'bg-amber-500';
|
||||||
return 'bg-green-600';
|
return 'bg-neutral-900';
|
||||||
}, [usage.isBlocked, usage.isWarning]);
|
}, [usage.isBlocked, usage.isWarning]);
|
||||||
|
|
||||||
const statusIcon = useMemo(() => {
|
const statusIcon = useMemo(() => {
|
||||||
@@ -440,10 +440,10 @@ const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: Usa
|
|||||||
<Progress value={Math.min(usage.percentage, 100)} className="h-2" indicatorClassName={progressColor} />
|
<Progress value={Math.min(usage.percentage, 100)} className="h-2" indicatorClassName={progressColor} />
|
||||||
|
|
||||||
{usage.isBlocked && (
|
{usage.isBlocked && (
|
||||||
<Alert className="mt-3 bg-red-50 border-red-200 text-red-900">
|
<Alert variant="destructive" className="mt-3">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<p className={'text-sm'}>
|
<p className="text-sm">
|
||||||
<strong>Limit reached:</strong> No more {category.toLowerCase()} emails can be sent this month.
|
<strong>Limit reached:</strong> No more {category.toLowerCase()} emails can be sent this month.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -451,7 +451,7 @@ const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: Usa
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{usage.isWarning && !usage.isBlocked && (
|
{usage.isWarning && !usage.isBlocked && (
|
||||||
<Alert className="mt-3 bg-orange-50 border-orange-200 text-orange-900">
|
<Alert variant="warning" className="mt-3">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
EmptyState,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -26,7 +27,6 @@ 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';
|
||||||
import {EmptyState} from './EmptyState';
|
|
||||||
|
|
||||||
interface CampaignSelectionDialogProps {
|
interface CampaignSelectionDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -97,23 +97,16 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: CampaignStatus) => {
|
const getStatusBadge = (status: CampaignStatus) => {
|
||||||
const variants: Record<
|
const variants: Record<CampaignStatus, {variant: 'neutral' | 'default' | 'success'; label: string}> = {
|
||||||
CampaignStatus,
|
DRAFT: {variant: 'neutral', label: 'Draft'},
|
||||||
{variant: 'default' | 'secondary' | 'destructive' | 'outline'; label: string; className?: string}
|
SCHEDULED: {variant: 'default', label: 'Scheduled'},
|
||||||
> = {
|
SENDING: {variant: 'default', label: 'Sending'},
|
||||||
DRAFT: {variant: 'secondary', label: 'Draft', className: 'bg-neutral-100 text-neutral-700'},
|
SENT: {variant: 'success', label: 'Sent'},
|
||||||
SCHEDULED: {variant: 'default', label: 'Scheduled', className: 'bg-blue-100 text-blue-700'},
|
CANCELLED: {variant: 'neutral', label: 'Cancelled'},
|
||||||
SENDING: {variant: 'default', label: 'Sending', className: 'bg-purple-100 text-purple-700'},
|
|
||||||
SENT: {variant: 'default', label: 'Sent', className: 'bg-green-100 text-green-700'},
|
|
||||||
CANCELLED: {variant: 'destructive', label: 'Cancelled', className: 'bg-red-100 text-red-700'},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = variants[status];
|
const config = variants[status];
|
||||||
return (
|
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||||||
<Badge variant={config.variant} className={config.className}>
|
|
||||||
{config.label}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAudienceLabel = (campaign: Campaign) => {
|
const getAudienceLabel = (campaign: Campaign) => {
|
||||||
@@ -132,7 +125,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
{step === 'configure' && (
|
{step === 'configure' && (
|
||||||
<Button variant="ghost" size="sm" onClick={handleBack} className="h-8 w-8 p-0">
|
<Button variant="ghost" size="icon" onClick={handleBack}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
EmptyState,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
IconSpinner,
|
IconSpinner,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AlertCircle, Database, Trash2, Zap} from 'lucide-react';
|
import {AlertCircle, Database, Trash2, Zap} from 'lucide-react';
|
||||||
import {EmptyState} from './EmptyState';
|
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {useActiveProject} from '../lib/contexts/ActiveProjectProvider';
|
import {useActiveProject} from '../lib/contexts/ActiveProjectProvider';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
EmptyState,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AnimatePresence, motion} from 'framer-motion';
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
import {Check, CheckCircle2, ChevronDown, Copy, Globe, 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 {useConfig} from '../lib/hooks/useConfig';
|
||||||
import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains';
|
import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains';
|
||||||
|
|
||||||
@@ -369,13 +369,12 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="destructiveGhost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDomainToRemove({id: domain.id, name: domain.domain});
|
setDomainToRemove({id: domain.id, name: domain.domain});
|
||||||
setShowRemoveDialog(true);
|
setShowRemoveDialog(true);
|
||||||
}}
|
}}
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import type {LucideIcon} from 'lucide-react';
|
|
||||||
import type {ReactNode} from 'react';
|
|
||||||
|
|
||||||
interface EmptyStateProps {
|
|
||||||
icon: LucideIcon;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
action?: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmptyState({icon: Icon, title, description, action, className}: EmptyStateProps) {
|
|
||||||
return (
|
|
||||||
<div className={`text-center py-14 ${className ?? ''}`}>
|
|
||||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-md border border-neutral-200 bg-neutral-50 mb-4">
|
|
||||||
<Icon className="h-5 w-5 text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-sm font-semibold text-neutral-900 mb-1">{title}</h3>
|
|
||||||
<p className="text-sm text-neutral-500 max-w-xs mx-auto leading-relaxed mb-5">{description}</p>
|
|
||||||
{action}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -114,13 +114,7 @@ export function KeyValueEditor({initialData, onChange}: KeyValueEditorProps) {
|
|||||||
className="text-sm"
|
className="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" variant="destructiveGhost" size="icon" onClick={() => removePair(pair.id)}>
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => removePair(pair.id)}
|
|
||||||
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface SecuritySettingsProps {
|
|||||||
|
|
||||||
const STATUS_CONFIG: Record<SecurityLevel, {color: string; icon: typeof CheckCircle; bg: string; label: string}> = {
|
const STATUS_CONFIG: Record<SecurityLevel, {color: string; icon: typeof CheckCircle; bg: string; label: string}> = {
|
||||||
healthy: {color: 'text-green-600', icon: CheckCircle, bg: 'bg-green-100', label: 'Healthy'},
|
healthy: {color: 'text-green-600', icon: CheckCircle, bg: 'bg-green-100', label: 'Healthy'},
|
||||||
warning: {color: 'text-orange-600', icon: AlertTriangle, bg: 'bg-orange-100', label: 'Warning'},
|
warning: {color: 'text-amber-700', icon: AlertTriangle, bg: 'bg-amber-100', label: 'Warning'},
|
||||||
critical: {color: 'text-red-600', icon: AlertCircle, bg: 'bg-red-100', label: 'Critical'},
|
critical: {color: 'text-red-600', icon: AlertCircle, bg: 'bg-red-100', label: 'Critical'},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -528,13 +528,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="button" variant="destructiveGhost" size="sm" onClick={onRemove} className="mt-6">
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="mt-6 text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,12 +597,12 @@ function FilterGroupComponent({group, onChange, onRemove, depth = 0, availableFi
|
|||||||
onChange(rest);
|
onChange(rest);
|
||||||
}, [group, onChange]);
|
}, [group, onChange]);
|
||||||
|
|
||||||
const bgColors = ['bg-white', 'bg-blue-50/50', 'bg-purple-50/50', 'bg-green-50/50'];
|
const bgColors = ['bg-white', 'bg-neutral-50', 'bg-white', 'bg-neutral-50'];
|
||||||
const borderColors = ['border-neutral-300', 'border-blue-300', 'border-purple-300', 'border-green-300'];
|
const borderColors = ['border-neutral-200', 'border-neutral-200', 'border-neutral-300', 'border-neutral-300'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`p-4 rounded-lg border-2 ${borderColors[depth % borderColors.length]} ${bgColors[depth % bgColors.length]}`}
|
className={`p-4 rounded-lg border ${borderColors[depth % borderColors.length]} ${bgColors[depth % bgColors.length]}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -616,13 +610,7 @@ function FilterGroupComponent({group, onChange, onRemove, depth = 0, availableFi
|
|||||||
<span className="text-sm font-medium text-neutral-700">Filter Group {depth > 0 && `(Nested)`}</span>
|
<span className="text-sm font-medium text-neutral-700">Filter Group {depth > 0 && `(Nested)`}</span>
|
||||||
</div>
|
</div>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<Button
|
<Button type="button" variant="destructiveGhost" size="sm" onClick={onRemove}>
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -645,10 +633,10 @@ function FilterGroupComponent({group, onChange, onRemove, depth = 0, availableFi
|
|||||||
<span className="text-xs font-medium text-neutral-600 uppercase tracking-wide">Nested Conditions</span>
|
<span className="text-xs font-medium text-neutral-600 uppercase tracking-wide">Nested Conditions</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="destructiveGhost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={removeNestedCondition}
|
onClick={removeNestedCondition}
|
||||||
className="h-6 text-xs text-red-600 hover:text-red-700 hover:bg-red-50"
|
className="h-6 text-xs"
|
||||||
>
|
>
|
||||||
Remove nested
|
Remove nested
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
EmptyState,
|
||||||
IconSpinner,
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
@@ -21,7 +22,6 @@ 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';
|
||||||
import {EmptyState} from './EmptyState';
|
|
||||||
|
|
||||||
interface TemplateSelectionDialogProps {
|
interface TemplateSelectionDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -106,7 +106,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
{step === 'configure' && (
|
{step === 'configure' && (
|
||||||
<Button variant="ghost" size="sm" onClick={handleBack} className="h-8 w-8 p-0">
|
<Button variant="ghost" size="icon" onClick={handleBack}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -228,7 +228,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<CardTitle className="text-base truncate">{template.name}</CardTitle>
|
<CardTitle className="text-base truncate">{template.name}</CardTitle>
|
||||||
<Badge className="capitalize" variant={template.type === 'MARKETING' ? 'info' : template.type === 'HEADLESS' ? 'warning' : 'success'}>
|
<Badge className="capitalize" variant="neutral">
|
||||||
{template.type.toLowerCase()}
|
{template.type.toLowerCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,10 +285,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
|||||||
<div className="pb-4 mb-1 border-b border-neutral-100">
|
<div className="pb-4 mb-1 border-b border-neutral-100">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium text-neutral-900">{selectedTemplate.name}</span>
|
<span className="text-sm font-medium text-neutral-900">{selectedTemplate.name}</span>
|
||||||
<Badge
|
<Badge className="capitalize" variant="neutral">
|
||||||
className="capitalize"
|
|
||||||
variant={selectedTemplate.type === 'MARKETING' ? 'info' : 'success'}
|
|
||||||
>
|
|
||||||
{selectedTemplate.type.toLowerCase()}
|
{selectedTemplate.type.toLowerCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,12 +73,7 @@ export function UnpaidInvoiceBanner({projectId, hasSubscription}: UnpaidInvoiceB
|
|||||||
Please pay your outstanding invoices to avoid service interruption.
|
Please pay your outstanding invoices to avoid service interruption.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="destructive" size="sm" onClick={handlePayNow} className="ml-4">
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
onClick={handlePayNow}
|
|
||||||
className="ml-4 bg-red-600 hover:bg-red-700 text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
Pay Now
|
Pay Now
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,32 +42,32 @@ export default function ActivityPage() {
|
|||||||
value: stats?.totalEvents?.toLocaleString() || '0',
|
value: stats?.totalEvents?.toLocaleString() || '0',
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
description: 'Last 30 days',
|
description: 'Last 30 days',
|
||||||
color: 'text-blue-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-blue-100',
|
bgColor: 'bg-neutral-100',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Emails Sent',
|
name: 'Emails Sent',
|
||||||
value: stats?.totalEmailsSent?.toLocaleString() || '0',
|
value: stats?.totalEmailsSent?.toLocaleString() || '0',
|
||||||
icon: Send,
|
icon: Send,
|
||||||
description: 'Last 30 days',
|
description: 'Last 30 days',
|
||||||
color: 'text-green-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-green-100',
|
bgColor: 'bg-neutral-100',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Open Rate',
|
name: 'Open Rate',
|
||||||
value: stats?.openRate ? `${stats.openRate.toFixed(1)}%` : '0%',
|
value: stats?.openRate ? `${stats.openRate.toFixed(1)}%` : '0%',
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
description: `${stats?.totalEmailsOpened?.toLocaleString() || '0'} opens`,
|
description: `${stats?.totalEmailsOpened?.toLocaleString() || '0'} opens`,
|
||||||
color: 'text-purple-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-purple-100',
|
bgColor: 'bg-neutral-100',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Click Rate',
|
name: 'Click Rate',
|
||||||
value: stats?.clickRate ? `${stats.clickRate.toFixed(1)}%` : '0%',
|
value: stats?.clickRate ? `${stats.clickRate.toFixed(1)}%` : '0%',
|
||||||
icon: MousePointerClick,
|
icon: MousePointerClick,
|
||||||
description: `${stats?.totalEmailsClicked?.toLocaleString() || '0'} clicks`,
|
description: `${stats?.totalEmailsClicked?.toLocaleString() || '0'} clicks`,
|
||||||
color: 'text-orange-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-orange-100',
|
bgColor: 'bg-neutral-100',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
|
import {EmptyState} from '@plunk/ui';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {useAnalytics} from '../../lib/hooks/useAnalytics';
|
import {useAnalytics} from '../../lib/hooks/useAnalytics';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
@@ -149,8 +149,8 @@ export default function AnalyticsPage() {
|
|||||||
value: stats?.totalEmailsSent?.toLocaleString() || cumulativeTotals.emails.toLocaleString(),
|
value: stats?.totalEmailsSent?.toLocaleString() || cumulativeTotals.emails.toLocaleString(),
|
||||||
icon: Send,
|
icon: Send,
|
||||||
description: `Last ${days} days`,
|
description: `Last ${days} days`,
|
||||||
color: 'text-blue-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-blue-100',
|
bgColor: 'bg-neutral-100',
|
||||||
trend: stats?.totalEmailsSent ? (stats.totalEmailsSent > 0 ? 'positive' : 'neutral') : 'neutral',
|
trend: stats?.totalEmailsSent ? (stats.totalEmailsSent > 0 ? 'positive' : 'neutral') : 'neutral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,8 +158,8 @@ export default function AnalyticsPage() {
|
|||||||
value: stats?.openRate ? `${stats.openRate.toFixed(1)}%` : '0%',
|
value: stats?.openRate ? `${stats.openRate.toFixed(1)}%` : '0%',
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
description: `${stats?.totalEmailsOpened?.toLocaleString() || cumulativeTotals.opens.toLocaleString()} opens`,
|
description: `${stats?.totalEmailsOpened?.toLocaleString() || cumulativeTotals.opens.toLocaleString()} opens`,
|
||||||
color: 'text-green-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-green-100',
|
bgColor: 'bg-neutral-100',
|
||||||
trend: stats?.openRate && stats.openRate > 20 ? 'positive' : 'neutral',
|
trend: stats?.openRate && stats.openRate > 20 ? 'positive' : 'neutral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -167,8 +167,8 @@ export default function AnalyticsPage() {
|
|||||||
value: stats?.clickRate ? `${stats.clickRate.toFixed(1)}%` : '0%',
|
value: stats?.clickRate ? `${stats.clickRate.toFixed(1)}%` : '0%',
|
||||||
icon: MousePointerClick,
|
icon: MousePointerClick,
|
||||||
description: `${stats?.totalEmailsClicked?.toLocaleString() || cumulativeTotals.clicks.toLocaleString()} clicks`,
|
description: `${stats?.totalEmailsClicked?.toLocaleString() || cumulativeTotals.clicks.toLocaleString()} clicks`,
|
||||||
color: 'text-purple-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-purple-100',
|
bgColor: 'bg-neutral-100',
|
||||||
trend: stats?.clickRate && stats.clickRate > 3 ? 'positive' : 'neutral',
|
trend: stats?.clickRate && stats.clickRate > 3 ? 'positive' : 'neutral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -176,8 +176,8 @@ export default function AnalyticsPage() {
|
|||||||
value: campaignStats?.active?.toLocaleString() || '0',
|
value: campaignStats?.active?.toLocaleString() || '0',
|
||||||
icon: Megaphone,
|
icon: Megaphone,
|
||||||
description: `${campaignStats?.total || 0} total campaigns`,
|
description: `${campaignStats?.total || 0} total campaigns`,
|
||||||
color: 'text-indigo-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-indigo-100',
|
bgColor: 'bg-neutral-100',
|
||||||
trend: campaignStats?.active ? 'positive' : 'neutral',
|
trend: campaignStats?.active ? 'positive' : 'neutral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -185,8 +185,8 @@ export default function AnalyticsPage() {
|
|||||||
value: stats?.totalWorkflowsStarted?.toLocaleString() || '0',
|
value: stats?.totalWorkflowsStarted?.toLocaleString() || '0',
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
description: 'Automations triggered',
|
description: 'Automations triggered',
|
||||||
color: 'text-cyan-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-cyan-100',
|
bgColor: 'bg-neutral-100',
|
||||||
trend: stats?.totalWorkflowsStarted && stats.totalWorkflowsStarted > 0 ? 'positive' : 'neutral',
|
trend: stats?.totalWorkflowsStarted && stats.totalWorkflowsStarted > 0 ? 'positive' : 'neutral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -194,8 +194,8 @@ export default function AnalyticsPage() {
|
|||||||
value: stats?.totalEvents?.toLocaleString() || '0',
|
value: stats?.totalEvents?.toLocaleString() || '0',
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
description: 'Custom events tracked',
|
description: 'Custom events tracked',
|
||||||
color: 'text-orange-600',
|
color: 'text-neutral-600',
|
||||||
bgColor: 'bg-orange-100',
|
bgColor: 'bg-neutral-100',
|
||||||
trend: stats?.totalEvents && stats.totalEvents > 0 ? 'positive' : 'neutral',
|
trend: stats?.totalEvents && stats.totalEvents > 0 ? 'positive' : 'neutral',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -457,8 +457,8 @@ export default function AnalyticsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="h-8 w-8 rounded-lg bg-green-100 flex items-center justify-center flex-shrink-0">
|
<div className="h-8 w-8 rounded-lg bg-neutral-100 flex items-center justify-center flex-shrink-0">
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
<CheckCircle2 className="h-4 w-4 text-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-neutral-900">Open Rate</p>
|
<p className="text-sm font-medium text-neutral-900">Open Rate</p>
|
||||||
@@ -470,8 +470,8 @@ export default function AnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="h-8 w-8 rounded-lg bg-blue-100 flex items-center justify-center flex-shrink-0">
|
<div className="h-8 w-8 rounded-lg bg-neutral-100 flex items-center justify-center flex-shrink-0">
|
||||||
<BarChart3 className="h-4 w-4 text-blue-600" />
|
<BarChart3 className="h-4 w-4 text-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-neutral-900">Click Rate</p>
|
<p className="text-sm font-medium text-neutral-900">Click Rate</p>
|
||||||
@@ -483,8 +483,8 @@ export default function AnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="h-8 w-8 rounded-lg bg-purple-100 flex items-center justify-center flex-shrink-0">
|
<div className="h-8 w-8 rounded-lg bg-neutral-100 flex items-center justify-center flex-shrink-0">
|
||||||
<Zap className="h-4 w-4 text-purple-600" />
|
<Zap className="h-4 w-4 text-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-neutral-900">Engagement</p>
|
<p className="text-sm font-medium text-neutral-900">Engagement</p>
|
||||||
@@ -509,8 +509,8 @@ export default function AnalyticsPage() {
|
|||||||
<p className="text-2xl font-bold text-neutral-900">{stats?.totalEvents?.toLocaleString() || '0'}</p>
|
<p className="text-2xl font-bold text-neutral-900">{stats?.totalEvents?.toLocaleString() || '0'}</p>
|
||||||
<p className="text-sm text-neutral-500">Total Events</p>
|
<p className="text-sm text-neutral-500">Total Events</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-12 w-12 rounded-lg bg-orange-100 flex items-center justify-center">
|
<div className="h-12 w-12 rounded-lg bg-neutral-100 flex items-center justify-center">
|
||||||
<Zap className="h-6 w-6 text-orange-600" />
|
<Zap className="h-6 w-6 text-neutral-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t">
|
<div className="pt-4 border-t">
|
||||||
@@ -566,11 +566,7 @@ export default function AnalyticsPage() {
|
|||||||
<td className="text-right py-3 px-4">
|
<td className="text-right py-3 px-4">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
campaign.openRate > 30
|
campaign.openRate > 30 ? 'text-green-700' : 'text-neutral-700'
|
||||||
? 'text-green-600'
|
|
||||||
: campaign.openRate > 20
|
|
||||||
? 'text-blue-600'
|
|
||||||
: 'text-neutral-600'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{campaign.openRate.toFixed(1)}%
|
{campaign.openRate.toFixed(1)}%
|
||||||
@@ -579,11 +575,7 @@ export default function AnalyticsPage() {
|
|||||||
<td className="text-right py-3 px-4">
|
<td className="text-right py-3 px-4">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
campaign.clickRate > 5
|
campaign.clickRate > 5 ? 'text-green-700' : 'text-neutral-700'
|
||||||
? 'text-green-600'
|
|
||||||
: campaign.clickRate > 3
|
|
||||||
? 'text-blue-600'
|
|
||||||
: 'text-neutral-600'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{campaign.clickRate.toFixed(1)}%
|
{campaign.clickRate.toFixed(1)}%
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
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 type {PaginatedResponse} from '@plunk/types';
|
||||||
|
import {EmptyState} from '@plunk/ui';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {TemplateSelectionDialog} from '../../components/TemplateSelectionDialog';
|
import {TemplateSelectionDialog} from '../../components/TemplateSelectionDialog';
|
||||||
import {CampaignSelectionDialog} from '../../components/CampaignSelectionDialog';
|
import {CampaignSelectionDialog} from '../../components/CampaignSelectionDialog';
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
@@ -49,15 +49,15 @@ export default function CampaignsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getStatusBadge = (status: CampaignStatus) => {
|
const getStatusBadge = (status: CampaignStatus) => {
|
||||||
const config: Record<CampaignStatus, {label: string; className: string}> = {
|
const config: Record<CampaignStatus, {label: string; variant: 'neutral' | 'default' | 'success'}> = {
|
||||||
DRAFT: {label: 'Draft', className: 'bg-neutral-100 text-neutral-600 border-transparent'},
|
DRAFT: {label: 'Draft', variant: 'neutral'},
|
||||||
SCHEDULED: {label: 'Scheduled', className: 'bg-blue-100 text-blue-700 border-transparent'},
|
SCHEDULED: {label: 'Scheduled', variant: 'default'},
|
||||||
SENDING: {label: 'Sending', className: 'bg-purple-100 text-purple-700 border-transparent'},
|
SENDING: {label: 'Sending', variant: 'default'},
|
||||||
SENT: {label: 'Sent', className: 'bg-green-100 text-green-700 border-transparent'},
|
SENT: {label: 'Sent', variant: 'success'},
|
||||||
CANCELLED: {label: 'Cancelled', className: 'bg-red-100 text-red-700 border-transparent'},
|
CANCELLED: {label: 'Cancelled', variant: 'neutral'},
|
||||||
};
|
};
|
||||||
const {label, className} = config[status];
|
const {label, variant} = config[status];
|
||||||
return <Badge className={`${className} shrink-0`}>{label}</Badge>;
|
return <Badge variant={variant} className="shrink-0">{label}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import type {Contact} from '@plunk/db';
|
import type {Contact} from '@plunk/db';
|
||||||
import type {CursorPaginatedResponse} from '@plunk/types';
|
import type {CursorPaginatedResponse} from '@plunk/types';
|
||||||
|
import {EmptyState} from '@plunk/ui';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {KeyValueEditor} from '../../components/KeyValueEditor';
|
import {KeyValueEditor} from '../../components/KeyValueEditor';
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
import {formatRelativeTime} from '../../lib/dateUtils';
|
import {formatRelativeTime} from '../../lib/dateUtils';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
Skeleton,
|
||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {AlertCircle, Mail, Send, TrendingUp, Users} from 'lucide-react';
|
import {AlertCircle, Mail, Send, TrendingUp, Users} from 'lucide-react';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
@@ -192,11 +193,7 @@ export default function Index() {
|
|||||||
<Icon className="h-4 w-4 text-neutral-500" />
|
<Icon className="h-4 w-4 text-neutral-500" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl tabular-nums">
|
<CardTitle className="text-2xl tabular-nums">
|
||||||
{isLoading ? (
|
{isLoading ? <Skeleton className="h-7 w-16" /> : stat.value}
|
||||||
<div className="h-7 w-16 bg-neutral-100 rounded animate-pulse" />
|
|
||||||
) : (
|
|
||||||
stat.value
|
|
||||||
)}
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Button} from '@plunk/ui';
|
import {Button, Skeleton} from '@plunk/ui';
|
||||||
import {motion} from 'framer-motion';
|
import {motion} from 'framer-motion';
|
||||||
import {ArrowLeft, ArrowRight, KeyRound, Mail, ShieldCheck} from 'lucide-react';
|
import {ArrowLeft, ArrowRight, KeyRound, Mail, ShieldCheck} from 'lucide-react';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
@@ -192,7 +192,7 @@ export default function OnboardingDeveloper() {
|
|||||||
isSecret
|
isSecret
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-16 animate-pulse rounded-lg bg-neutral-100" />
|
<Skeleton className="h-16 rounded-lg" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Button} from '@plunk/ui';
|
import {Button, Skeleton} from '@plunk/ui';
|
||||||
import {motion} from 'framer-motion';
|
import {motion} from 'framer-motion';
|
||||||
import {ArrowLeft, ArrowRight, FileText, ShieldCheck, Workflow, Zap} from 'lucide-react';
|
import {ArrowLeft, ArrowRight, FileText, ShieldCheck, Workflow, Zap} from 'lucide-react';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
@@ -201,7 +201,7 @@ export default function OnboardingWorkflows() {
|
|||||||
description="Use this key to track events from browsers or clients."
|
description="Use this key to track events from browsers or clients."
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-16 animate-pulse rounded-lg bg-neutral-100" />
|
<Skeleton className="h-16 rounded-lg" />
|
||||||
)}
|
)}
|
||||||
<div className="overflow-hidden rounded-lg border border-neutral-200">
|
<div className="overflow-hidden rounded-lg border border-neutral-200">
|
||||||
<CodeTabs snippets={snippets} />
|
<CodeTabs snippets={snippets} />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -219,14 +220,10 @@ export default function SegmentDetailPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-3xl font-bold text-neutral-900">{segment.name}</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">{segment.name}</h1>
|
||||||
<span
|
<Badge variant={isStatic ? 'neutral' : 'default'}>
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
isStatic ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isStatic ? 'Static' : 'Dynamic'}
|
{isStatic ? 'Static' : 'Dynamic'}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{segment.description && <p className="text-neutral-500 mt-1">{segment.description}</p>}
|
{segment.description && <p className="text-neutral-500 mt-1">{segment.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
@@ -395,11 +392,10 @@ export default function SegmentDetailPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
{isStatic && (
|
{isStatic && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="destructiveGhost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRemoveMember(contact.email)}
|
onClick={() => handleRemoveMember(contact.email)}
|
||||||
disabled={removingEmail === contact.email}
|
disabled={removingEmail === contact.email}
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
>
|
||||||
<UserMinus className="h-4 w-4" />
|
<UserMinus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} 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';
|
||||||
|
import {EmptyState} from '@plunk/ui';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
import {formatRelativeTime} from '../../lib/dateUtils';
|
import {formatRelativeTime} from '../../lib/dateUtils';
|
||||||
import {AlertTriangle, Calendar, Edit, Filter, Plus, Search, Trash2, X} from 'lucide-react';
|
import {AlertTriangle, Calendar, Edit, Filter, Plus, Search, Trash2, X} from 'lucide-react';
|
||||||
@@ -170,7 +170,7 @@ export default function SegmentsPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
<h3 className="font-semibold text-neutral-900 leading-snug">{segment.name}</h3>
|
<h3 className="font-semibold text-neutral-900 leading-snug">{segment.name}</h3>
|
||||||
<Badge variant={isDynamic ? 'info' : 'default'} className="shrink-0 mt-0.5">
|
<Badge variant={isDynamic ? 'default' : 'neutral'} className="shrink-0 mt-0.5">
|
||||||
{isDynamic ? 'Dynamic' : 'Static'}
|
{isDynamic ? 'Dynamic' : 'Static'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function NewSegmentPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-neutral-900">Create Segment</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Segment</h1>
|
||||||
<p className="text-neutral-500 mt-1">
|
<p className="text-neutral-500 mt-1">
|
||||||
{segmentType === 'DYNAMIC'
|
{segmentType === 'DYNAMIC'
|
||||||
? 'Build complex audience filters with AND/OR logic'
|
? 'Build complex audience filters with AND/OR logic'
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ export default function Settings() {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-neutral-900">Settings</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Settings</h1>
|
||||||
<p className="text-neutral-500 mt-2">Manage your project settings and preferences</p>
|
<p className="text-neutral-500 mt-2">Manage your project settings and preferences</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -574,7 +574,7 @@ export default function Settings() {
|
|||||||
<div className="flex items-start justify-between gap-4 p-5 rounded-lg border border-neutral-200 bg-white transition-all">
|
<div className="flex items-start justify-between gap-4 p-5 rounded-lg border border-neutral-200 bg-white transition-all">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Database className="h-4 w-4 text-orange-600" />
|
<Database className="h-4 w-4 text-amber-700" />
|
||||||
<h4 className="font-semibold text-neutral-900">Reset Project Data</h4>
|
<h4 className="font-semibold text-neutral-900">Reset Project Data</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-600 mb-3">
|
<p className="text-sm text-neutral-600 mb-3">
|
||||||
@@ -586,12 +586,7 @@ export default function Settings() {
|
|||||||
<span>API keys, domains, billing information</span>
|
<span>API keys, domains, billing information</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" variant="outline" onClick={() => setShowResetDialog(true)} className="shrink-0">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setShowResetDialog(true)}
|
|
||||||
className="shrink-0 border-orange-300 text-orange-700 hover:bg-orange-50 hover:border-orange-400"
|
|
||||||
>
|
|
||||||
Reset Data
|
Reset Data
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -823,7 +818,7 @@ export default function Settings() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<AlertTriangle className="h-5 w-5 text-orange-500" />
|
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
||||||
Regenerate API Keys
|
Regenerate API Keys
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="space-y-3">
|
<DialogDescription className="space-y-3">
|
||||||
@@ -841,7 +836,7 @@ export default function Settings() {
|
|||||||
<Button variant="outline" onClick={() => setShowRegenerateDialog(false)}>
|
<Button variant="outline" onClick={() => setShowRegenerateDialog(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" onClick={handleRegenerateKeys} className="bg-orange-600 hover:bg-orange-700">
|
<Button variant="destructive" onClick={handleRegenerateKeys}>
|
||||||
Regenerate Keys
|
Regenerate Keys
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -852,8 +847,8 @@ export default function Settings() {
|
|||||||
<Dialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
<Dialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader className="space-y-3">
|
<DialogHeader className="space-y-3">
|
||||||
<div className="mx-auto w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center">
|
<div className="mx-auto w-12 h-12 bg-amber-100 rounded-full flex items-center justify-center">
|
||||||
<Database className="h-6 w-6 text-orange-600" />
|
<Database className="h-6 w-6 text-amber-700" />
|
||||||
</div>
|
</div>
|
||||||
<DialogTitle className="text-center text-xl">Reset Project Data?</DialogTitle>
|
<DialogTitle className="text-center text-xl">Reset Project Data?</DialogTitle>
|
||||||
<DialogDescription className="text-center text-base">
|
<DialogDescription className="text-center text-base">
|
||||||
@@ -865,7 +860,7 @@ export default function Settings() {
|
|||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<label className="text-sm font-medium text-neutral-700 block mb-2 text-center">
|
<label className="text-sm font-medium text-neutral-700 block mb-2 text-center">
|
||||||
Type{' '}
|
Type{' '}
|
||||||
<span className="font-mono font-semibold text-orange-600 bg-orange-50 px-2 py-0.5 rounded">RESET</span>{' '}
|
<span className="font-mono font-semibold text-amber-700 bg-amber-50 px-2 py-0.5 rounded">RESET</span>{' '}
|
||||||
to confirm
|
to confirm
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -889,9 +884,10 @@ export default function Settings() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant="destructive"
|
||||||
onClick={handleResetProject}
|
onClick={handleResetProject}
|
||||||
disabled={resetConfirmText !== 'RESET'}
|
disabled={resetConfirmText !== 'RESET'}
|
||||||
className="w-full bg-orange-600 hover:bg-orange-700 disabled:opacity-50"
|
className="w-full"
|
||||||
>
|
>
|
||||||
Reset Data
|
Reset Data
|
||||||
</Button>
|
</Button>
|
||||||
@@ -946,9 +942,10 @@ export default function Settings() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant="destructive"
|
||||||
onClick={handleDeleteProject}
|
onClick={handleDeleteProject}
|
||||||
disabled={deleteConfirmText !== 'DELETE'}
|
disabled={deleteConfirmText !== 'DELETE'}
|
||||||
className="w-full bg-red-600 hover:bg-red-700 disabled:opacity-50"
|
className="w-full"
|
||||||
>
|
>
|
||||||
Delete Forever
|
Delete Forever
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
} 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 type {PaginatedResponse} from '@plunk/types';
|
||||||
|
import {EmptyState} from '@plunk/ui';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
import {formatRelativeTime} from '../../lib/dateUtils';
|
import {formatRelativeTime} from '../../lib/dateUtils';
|
||||||
import {Calendar, Copy, Edit, FileText, Plus, Search, Trash2, X} from 'lucide-react';
|
import {Calendar, Copy, Edit, FileText, Plus, Search, Trash2, X} from 'lucide-react';
|
||||||
@@ -174,7 +174,7 @@ export default function TemplatesPage() {
|
|||||||
<h3 className="font-semibold text-neutral-900 leading-snug">{template.name}</h3>
|
<h3 className="font-semibold text-neutral-900 leading-snug">{template.name}</h3>
|
||||||
<Badge
|
<Badge
|
||||||
className="capitalize shrink-0 mt-0.5"
|
className="capitalize shrink-0 mt-0.5"
|
||||||
variant={template.type === 'MARKETING' ? 'info' : template.type === 'HEADLESS' ? 'warning' : 'success'}
|
variant="neutral"
|
||||||
>
|
>
|
||||||
{template.type.toLowerCase()}
|
{template.type.toLowerCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
AlertDescription,
|
AlertDescription,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
EmptyState,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -36,7 +38,6 @@ import {
|
|||||||
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
|
import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
|
||||||
import type {PaginatedResponse} from '@plunk/types';
|
import type {PaginatedResponse} from '@plunk/types';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -659,23 +660,21 @@ export default function WorkflowEditorPage() {
|
|||||||
{execution.contact.email}
|
{execution.contact.email}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span
|
<Badge
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
variant={
|
||||||
execution.status === 'COMPLETED'
|
execution.status === 'COMPLETED'
|
||||||
? 'bg-green-100 text-green-800'
|
? 'success'
|
||||||
: execution.status === 'RUNNING'
|
|
||||||
? 'bg-blue-100 text-blue-800'
|
|
||||||
: execution.status === 'WAITING'
|
|
||||||
? 'bg-yellow-100 text-yellow-800'
|
|
||||||
: execution.status === 'FAILED'
|
: execution.status === 'FAILED'
|
||||||
? 'bg-red-100 text-red-800'
|
? 'destructive'
|
||||||
: execution.status === 'CANCELLED'
|
: execution.status === 'WAITING'
|
||||||
? 'bg-neutral-100 text-neutral-800'
|
? 'warning'
|
||||||
: 'bg-neutral-100 text-neutral-800'
|
: execution.status === 'RUNNING'
|
||||||
}`}
|
? 'default'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{execution.status}
|
{execution.status}
|
||||||
</span>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-neutral-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-neutral-500">
|
||||||
{execution.currentStep?.name ?? '-'}
|
{execution.currentStep?.name ?? '-'}
|
||||||
@@ -690,13 +689,14 @@ export default function WorkflowEditorPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
{(execution.status === 'RUNNING' || execution.status === 'WAITING') && (
|
{(execution.status === 'RUNNING' || execution.status === 'WAITING') && (
|
||||||
<button
|
<Button
|
||||||
|
variant="destructiveGhost"
|
||||||
|
size="sm"
|
||||||
onClick={() => setExecutionToCancel(execution.id)}
|
onClick={() => setExecutionToCancel(execution.id)}
|
||||||
className="text-red-600 hover:text-red-900 disabled:opacity-50"
|
|
||||||
disabled={isCancelling}
|
disabled={isCancelling}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -2471,13 +2471,15 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-semibold text-neutral-500">Branch {idx + 1}</span>
|
<span className="text-xs font-semibold text-neutral-500">Branch {idx + 1}</span>
|
||||||
{conditionBranches.length > 1 && (
|
{conditionBranches.length > 1 && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="destructiveGhost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
onClick={() => setConditionBranches(prev => prev.filter(b => b.id !== branch.id))}
|
onClick={() => setConditionBranches(prev => prev.filter(b => b.id !== branch.id))}
|
||||||
className="text-xs text-red-500 hover:text-red-700"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2551,7 +2553,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
{id: crypto.randomUUID().slice(0, 8), name: '', operator: 'equals', value: ''},
|
{id: crypto.randomUUID().slice(0, 8), name: '', operator: 'equals', value: ''},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
className="flex items-center gap-1.5 text-sm text-purple-600 hover:text-purple-800 font-medium"
|
className="flex items-center gap-1.5 text-sm text-neutral-700 hover:text-neutral-900 font-medium"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
Add Branch
|
Add Branch
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
} 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 type {PaginatedResponse} from '@plunk/types';
|
||||||
|
import {EmptyState} from '@plunk/ui';
|
||||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
import {EmptyState} from '../../components/EmptyState';
|
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
import {formatRelativeTime} from '../../lib/dateUtils';
|
import {formatRelativeTime} from '../../lib/dateUtils';
|
||||||
import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon, X, Zap} from 'lucide-react';
|
import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon, X, Zap} from 'lucide-react';
|
||||||
@@ -165,7 +165,7 @@ export default function WorkflowsPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
<h3 className="font-semibold text-neutral-900 leading-snug">{workflow.name}</h3>
|
<h3 className="font-semibold text-neutral-900 leading-snug">{workflow.name}</h3>
|
||||||
<Badge variant={workflow.enabled ? 'green' : 'neutral'} className="shrink-0 mt-0.5">
|
<Badge variant={workflow.enabled ? 'success' : 'neutral'} className="shrink-0 mt-0.5">
|
||||||
{workflow.enabled ? (
|
{workflow.enabled ? (
|
||||||
<><Power className="h-3 w-3 mr-1" />Active</>
|
<><Power className="h-3 w-3 mr-1" />Active</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const alertVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-white text-neutral-950',
|
default: 'bg-white text-neutral-950',
|
||||||
destructive: 'border-red-500/50 text-red-900 [&>svg]:text-red-900',
|
destructive: 'border-red-200 bg-red-50 text-red-900 [&>svg]:text-red-600',
|
||||||
warning: 'bg-amber-50 border-amber-200 text-amber-900 [&>svg]:text-amber-600',
|
warning: 'border-amber-200 bg-amber-50 text-amber-900 [&>svg]:text-amber-600',
|
||||||
|
success: 'border-green-200 bg-green-50 text-green-900 [&>svg]:text-green-600',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -4,22 +4,17 @@ import * as React from 'react';
|
|||||||
import {cn} from '../../lib';
|
import {cn} from '../../lib';
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-2',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'border-transparent bg-neutral-100 text-neutral-900',
|
default: 'border-transparent bg-neutral-100 text-neutral-900',
|
||||||
secondary: 'border-transparent bg-neutral-100 text-neutral-900',
|
secondary: 'border-transparent bg-neutral-100 text-neutral-900',
|
||||||
destructive: 'border-transparent bg-red-100 text-red-900',
|
|
||||||
outline: 'text-neutral-950',
|
outline: 'text-neutral-950',
|
||||||
success: 'border-transparent bg-green-100 text-green-900',
|
|
||||||
warning: 'border-transparent bg-yellow-100 text-yellow-900',
|
|
||||||
// Additional semantic variants for common use cases
|
|
||||||
info: 'border-transparent bg-blue-100 text-blue-900',
|
|
||||||
orange: 'border-transparent bg-orange-100 text-orange-800',
|
|
||||||
green: 'border-transparent bg-green-100 text-green-800',
|
|
||||||
red: 'border-transparent bg-red-100 text-red-800',
|
|
||||||
neutral: 'border-transparent bg-neutral-50 text-neutral-600',
|
neutral: 'border-transparent bg-neutral-50 text-neutral-600',
|
||||||
|
destructive: 'border-transparent bg-red-100 text-red-900',
|
||||||
|
success: 'border-transparent bg-green-100 text-green-900',
|
||||||
|
warning: 'border-transparent bg-amber-100 text-amber-900',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90',
|
default: 'bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90',
|
||||||
destructive: 'bg-red-500 text-neutral-50 hover:bg-red-500/90',
|
destructive: 'bg-red-600 text-white hover:bg-red-600/90',
|
||||||
outline: 'border border-neutral-200 bg-white hover:bg-neutral-100 hover:text-neutral-900',
|
outline: 'border border-neutral-200 bg-white hover:bg-neutral-100 hover:text-neutral-900',
|
||||||
secondary: 'bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80',
|
secondary: 'bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80',
|
||||||
ghost: 'hover:bg-neutral-100 hover:text-neutral-900',
|
ghost: 'hover:bg-neutral-100 hover:text-neutral-900',
|
||||||
|
destructiveGhost: 'text-red-600 hover:text-red-700 hover:bg-red-50',
|
||||||
link: 'text-neutral-900 underline-offset-4 hover:underline',
|
link: 'text-neutral-900 underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {cn} from '../../lib';
|
|||||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({className, ...props}, ref) => (
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({className, ...props}, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow-sm overflow-hidden', className)}
|
className={cn('rounded-lg border border-neutral-200 bg-white text-neutral-950 shadow-sm overflow-hidden', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50',
|
'peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-60 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 p-1.5">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md opacity-60 ring-offset-white transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 p-1.5">
|
||||||
<X className="h-5 w-5 sm:h-5 sm:w-5" />
|
<X className="h-5 w-5 sm:h-5 sm:w-5" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p ref={ref} id={formMessageId} className={cn('text-[0.8rem] font-medium text-red-500', className)} {...props}>
|
<p ref={ref} id={formMessageId} className={cn('text-[0.8rem] font-medium text-red-600', className)} {...props}>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const RadioGroupItem = React.forwardRef<
|
|||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'aspect-square h-4 w-4 rounded-full border border-neutral-200 text-neutral-900 ring-offset-white focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'aspect-square h-4 w-4 rounded-full border border-neutral-200 text-neutral-900 ring-offset-white focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full items-center justify-between rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
'flex h-10 w-full items-center justify-between rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Switch = React.forwardRef<
|
|||||||
>(({className, ...props}, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200',
|
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
|
|||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-neutral-950 data-[state=active]:shadow-sm',
|
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all hover:text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-neutral-950 data-[state=active]:shadow-sm',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<
|
|||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2',
|
'mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import type {LucideIcon} from 'lucide-react';
|
import type {LucideIcon} from 'lucide-react';
|
||||||
import * as React from 'react';
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
import {cn} from '../../lib';
|
import {cn} from '../../lib';
|
||||||
import {Button} from '../atoms';
|
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
export interface EmptyStateProps {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
action?: {
|
action?: ReactNode;
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmptyState({icon: Icon, title, description, action, className}: EmptyStateProps) {
|
export function EmptyState({icon: Icon, title, description, action, className}: EmptyStateProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('text-center py-12', className)}>
|
<div className={cn('text-center py-14', className)}>
|
||||||
<Icon className="h-12 w-12 mx-auto text-neutral-400 mb-4" />
|
<div className="inline-flex items-center justify-center w-10 h-10 rounded-md border border-neutral-200 bg-neutral-50 mb-4">
|
||||||
<h3 className="text-lg font-medium text-neutral-900 mb-2">{title}</h3>
|
<Icon className="h-5 w-5 text-neutral-400" />
|
||||||
{description && <p className="text-sm text-neutral-500 mb-6 max-w-md mx-auto">{description}</p>}
|
</div>
|
||||||
{action && (
|
<h3 className="text-sm font-semibold text-neutral-900 mb-1">{title}</h3>
|
||||||
<Button onClick={action.onClick} variant="default">
|
{description && (
|
||||||
{action.label}
|
<p className="text-sm text-neutral-500 max-w-xs mx-auto leading-relaxed mb-5">{description}</p>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
{action}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user