chore: Resolve linting warnings
This commit is contained in:
@@ -5,7 +5,6 @@ import signale from 'signale';
|
|||||||
|
|
||||||
import {prisma} from '../database/prisma.js';
|
import {prisma} from '../database/prisma.js';
|
||||||
import {redis} from '../database/redis.js';
|
import {redis} from '../database/redis.js';
|
||||||
import {ValidationError} from '../exceptions/index.js';
|
|
||||||
import {Keys} from './keys.js';
|
import {Keys} from './keys.js';
|
||||||
|
|
||||||
import {WorkflowExecutionService} from './WorkflowExecutionService.js';
|
import {WorkflowExecutionService} from './WorkflowExecutionService.js';
|
||||||
|
|||||||
@@ -1342,7 +1342,6 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
|||||||
it('should match contacts who have triggered the event', async () => {
|
it('should match contacts who have triggered the event', async () => {
|
||||||
const prisma = getPrismaClient();
|
const prisma = getPrismaClient();
|
||||||
const match = await factories.createContact({projectId});
|
const match = await factories.createContact({projectId});
|
||||||
const noMatch = await factories.createContact({projectId});
|
|
||||||
|
|
||||||
// Create event for match contact
|
// Create event for match contact
|
||||||
await prisma.event.create({
|
await prisma.event.create({
|
||||||
@@ -1365,7 +1364,6 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
|||||||
it('should not match contacts who have not triggered the event', async () => {
|
it('should not match contacts who have not triggered the event', async () => {
|
||||||
const prisma = getPrismaClient();
|
const prisma = getPrismaClient();
|
||||||
const contact1 = await factories.createContact({projectId});
|
const contact1 = await factories.createContact({projectId});
|
||||||
const contact2 = await factories.createContact({projectId});
|
|
||||||
|
|
||||||
// Create different event
|
// Create different event
|
||||||
await prisma.event.create({
|
await prisma.event.create({
|
||||||
@@ -1682,10 +1680,6 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
|||||||
projectId,
|
projectId,
|
||||||
data: {plan: 'basic'},
|
data: {plan: 'basic'},
|
||||||
});
|
});
|
||||||
const noMatch2 = await factories.createContact({
|
|
||||||
projectId,
|
|
||||||
data: {plan: 'premium'},
|
|
||||||
});
|
|
||||||
|
|
||||||
// match and noMatch1 have the event, but only match has premium plan
|
// match and noMatch1 have the event, but only match has premium plan
|
||||||
await prisma.event.create({
|
await prisma.event.create({
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import {motion} from 'framer-motion';
|
|
||||||
import React, {useState} from 'react';
|
|
||||||
|
|
||||||
interface PricingData {
|
|
||||||
name: string;
|
|
||||||
calculatePrice: (emails: number) => number;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PricingCalculatorProps {
|
|
||||||
competitors: PricingData[];
|
|
||||||
defaultVolume?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const volumeOptions = [
|
|
||||||
{label: '10K emails/month', value: 10000},
|
|
||||||
{label: '100K emails/month', value: 100000},
|
|
||||||
{label: '500K emails/month', value: 500000},
|
|
||||||
{label: '1M emails/month', value: 1000000},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interactive pricing calculator comparing Plunk with competitors
|
|
||||||
*/
|
|
||||||
export function PricingCalculator({competitors, defaultVolume = 100000}: PricingCalculatorProps) {
|
|
||||||
const [volume, setVolume] = useState(defaultVolume);
|
|
||||||
|
|
||||||
const plunkPrice = volume * 0.001;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{opacity: 0, y: 20}}
|
|
||||||
whileInView={{opacity: 1, y: 0}}
|
|
||||||
viewport={{once: true}}
|
|
||||||
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
|
|
||||||
className={'overflow-hidden rounded-xl border border-neutral-200 bg-white'}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className={'border-b border-neutral-200 bg-neutral-50 p-8'}>
|
|
||||||
<h3 className={'text-2xl font-bold text-neutral-900'}>Pricing Calculator</h3>
|
|
||||||
<p className={'mt-2 text-sm text-neutral-600'}>Compare costs across platforms at different email volumes</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Volume Selector */}
|
|
||||||
<div className={'p-8'}>
|
|
||||||
<label className={'block text-sm font-semibold text-neutral-900'}>Monthly Email Volume</label>
|
|
||||||
<div className={'mt-4 grid grid-cols-2 gap-3 sm:grid-cols-4'}>
|
|
||||||
{volumeOptions.map(option => (
|
|
||||||
<button
|
|
||||||
key={option.value}
|
|
||||||
onClick={() => setVolume(option.value)}
|
|
||||||
className={`rounded-lg border px-4 py-3 text-sm font-medium transition ${
|
|
||||||
volume === option.value
|
|
||||||
? 'border-neutral-900 bg-neutral-900 text-white'
|
|
||||||
: 'border-neutral-300 bg-white text-neutral-900 hover:border-neutral-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Results */}
|
|
||||||
<div className={'space-y-4 p-8 pt-0'}>
|
|
||||||
{/* Plunk */}
|
|
||||||
<div className={'rounded-lg border border-neutral-900 bg-neutral-50 p-6'}>
|
|
||||||
<div className={'flex items-center justify-between'}>
|
|
||||||
<div>
|
|
||||||
<span className={'text-lg font-bold text-neutral-900'}>Plunk</span>
|
|
||||||
<span className={'ml-3 rounded-full bg-neutral-900 px-3 py-1 text-xs font-medium text-white'}>
|
|
||||||
Cheapest
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={'text-right'}>
|
|
||||||
<div className={'text-3xl font-bold text-neutral-900'}>${plunkPrice.toFixed(2)}</div>
|
|
||||||
<div className={'text-sm text-neutral-600'}>per month</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Competitors */}
|
|
||||||
{competitors.map((competitor, index) => {
|
|
||||||
const price = competitor.calculatePrice(volume);
|
|
||||||
const savings = ((price - plunkPrice) / price) * 100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={competitor.name} className={'rounded-lg border border-neutral-200 bg-white p-6'}>
|
|
||||||
<div className={'flex items-center justify-between'}>
|
|
||||||
<div>
|
|
||||||
<span className={'text-lg font-semibold text-neutral-900'}>{competitor.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className={'text-right'}>
|
|
||||||
<div className={'text-3xl font-bold text-neutral-900'}>${price.toFixed(2)}</div>
|
|
||||||
<div className={'text-sm text-neutral-600'}>per month</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{savings > 0 && (
|
|
||||||
<div className={'mt-3 text-sm text-neutral-600'}>
|
|
||||||
Save <span className={'font-semibold text-neutral-900'}>{savings.toFixed(0)}%</span> with Plunk
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,4 +3,3 @@ export * from './Footer';
|
|||||||
export * from './ComparisonTable';
|
export * from './ComparisonTable';
|
||||||
export * from './FAQSection';
|
export * from './FAQSection';
|
||||||
export * from './CodeBlock';
|
export * from './CodeBlock';
|
||||||
export * from './PricingCalculator';
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
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 {DashboardLayout} from '../../components/DashboardLayout';
|
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Trash2,
|
Trash2,
|
||||||
Upload,
|
Upload,
|
||||||
XCircle
|
XCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -237,27 +237,15 @@ export default function ContactsPage() {
|
|||||||
{selectedContacts.size} contact{selectedContacts.size !== 1 ? 's' : ''} selected
|
{selectedContacts.size} contact{selectedContacts.size !== 1 ? 's' : ''} selected
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button variant="outline" size="sm" onClick={() => handleBulkAction('subscribe')}>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleBulkAction('subscribe')}
|
|
||||||
>
|
|
||||||
<MailCheck className="h-4 w-4 mr-1.5" />
|
<MailCheck className="h-4 w-4 mr-1.5" />
|
||||||
Subscribe
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" size="sm" onClick={() => handleBulkAction('unsubscribe')}>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleBulkAction('unsubscribe')}
|
|
||||||
>
|
|
||||||
<MailX className="h-4 w-4 mr-1.5" />
|
<MailX className="h-4 w-4 mr-1.5" />
|
||||||
Unsubscribe
|
Unsubscribe
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" size="sm" onClick={() => handleBulkAction('delete')}>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleBulkAction('delete')}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-1.5" />
|
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
@@ -625,8 +613,7 @@ interface ImportResult {
|
|||||||
|
|
||||||
function ImportContactsDialog({open, onOpenChange, onSuccess}: ImportContactsDialogProps) {
|
function ImportContactsDialog({open, onOpenChange, onSuccess}: ImportContactsDialogProps) {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const [, setJobId] = useState<string | null>(null);
|
||||||
const [jobId, setJobId] = useState<string | null>(null);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [status, setStatus] = useState<'idle' | 'uploading' | 'processing' | 'completed' | 'failed'>('idle');
|
const [status, setStatus] = useState<'idle' | 'uploading' | 'processing' | 'completed' | 'failed'>('idle');
|
||||||
@@ -978,7 +965,7 @@ interface BulkActionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess}: BulkActionsDialogProps) {
|
function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess}: BulkActionsDialogProps) {
|
||||||
const [jobId, setJobId] = useState<string | null>(null);
|
const [, setJobId] = useState<string | null>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
|
const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
|
||||||
@@ -1097,19 +1084,27 @@ function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess
|
|||||||
|
|
||||||
const getOperationLabel = () => {
|
const getOperationLabel = () => {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'subscribe': return 'Subscribe';
|
case 'subscribe':
|
||||||
case 'unsubscribe': return 'Unsubscribe';
|
return 'Subscribe';
|
||||||
case 'delete': return 'Delete';
|
case 'unsubscribe':
|
||||||
default: return 'Process';
|
return 'Unsubscribe';
|
||||||
|
case 'delete':
|
||||||
|
return 'Delete';
|
||||||
|
default:
|
||||||
|
return 'Process';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOperationColor = () => {
|
const getOperationColor = () => {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'subscribe': return 'green';
|
case 'subscribe':
|
||||||
case 'unsubscribe': return 'yellow';
|
return 'green';
|
||||||
case 'delete': return 'red';
|
case 'unsubscribe':
|
||||||
default: return 'blue';
|
return 'yellow';
|
||||||
|
case 'delete':
|
||||||
|
return 'red';
|
||||||
|
default:
|
||||||
|
return 'blue';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1129,14 +1124,12 @@ function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess
|
|||||||
{contactIds.length !== 1 ? 's' : ''}?
|
{contactIds.length !== 1 ? 's' : ''}?
|
||||||
</p>
|
</p>
|
||||||
{operation === 'delete' && (
|
{operation === 'delete' && (
|
||||||
<p className="text-sm text-red-600 mt-2 font-medium">
|
<p className="text-sm text-red-600 mt-2 font-medium">This action cannot be undone.</p>
|
||||||
This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(status === 'processing') && (
|
{status === 'processing' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-neutral-600">Processing contacts...</span>
|
<span className="text-neutral-600">Processing contacts...</span>
|
||||||
@@ -1196,9 +1189,7 @@ function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess
|
|||||||
<XCircle className="h-5 w-5" />
|
<XCircle className="h-5 w-5" />
|
||||||
<span className="font-medium">Operation failed</span>
|
<span className="font-medium">Operation failed</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-red-800 mt-1">
|
<p className="text-sm text-red-800 mt-1">{errorMessage || 'Please try again.'}</p>
|
||||||
{errorMessage || 'Please try again.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user