chore: Resolve linting warnings
This commit is contained in:
@@ -5,7 +5,6 @@ import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {redis} from '../database/redis.js';
|
||||
import {ValidationError} from '../exceptions/index.js';
|
||||
import {Keys} from './keys.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 () => {
|
||||
const prisma = getPrismaClient();
|
||||
const match = await factories.createContact({projectId});
|
||||
const noMatch = await factories.createContact({projectId});
|
||||
|
||||
// Create event for match contact
|
||||
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 () => {
|
||||
const prisma = getPrismaClient();
|
||||
const contact1 = await factories.createContact({projectId});
|
||||
const contact2 = await factories.createContact({projectId});
|
||||
|
||||
// Create different event
|
||||
await prisma.event.create({
|
||||
@@ -1682,10 +1680,6 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
projectId,
|
||||
data: {plan: 'basic'},
|
||||
});
|
||||
const noMatch2 = await factories.createContact({
|
||||
projectId,
|
||||
data: {plan: 'premium'},
|
||||
});
|
||||
|
||||
// match and noMatch1 have the event, but only match has premium plan
|
||||
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 './FAQSection';
|
||||
export * from './CodeBlock';
|
||||
export * from './PricingCalculator';
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Switch
|
||||
Switch,
|
||||
} from '@plunk/ui';
|
||||
import type {Contact} from '@plunk/db';
|
||||
import {DashboardLayout} from '../../components/DashboardLayout';
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
Search,
|
||||
Trash2,
|
||||
Upload,
|
||||
XCircle
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import {NextSeo} from 'next-seo';
|
||||
import Link from 'next/link';
|
||||
@@ -237,27 +237,15 @@ export default function ContactsPage() {
|
||||
{selectedContacts.size} contact{selectedContacts.size !== 1 ? 's' : ''} selected
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleBulkAction('subscribe')}
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={() => handleBulkAction('subscribe')}>
|
||||
<MailCheck className="h-4 w-4 mr-1.5" />
|
||||
Subscribe
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleBulkAction('unsubscribe')}
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={() => handleBulkAction('unsubscribe')}>
|
||||
<MailX className="h-4 w-4 mr-1.5" />
|
||||
Unsubscribe
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleBulkAction('delete')}
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={() => handleBulkAction('delete')}>
|
||||
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||
Delete
|
||||
</Button>
|
||||
@@ -625,8 +613,7 @@ interface ImportResult {
|
||||
|
||||
function ImportContactsDialog({open, onOpenChange, onSuccess}: ImportContactsDialogProps) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [, setJobId] = useState<string | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState<'idle' | 'uploading' | 'processing' | 'completed' | 'failed'>('idle');
|
||||
@@ -978,7 +965,7 @@ interface BulkActionResult {
|
||||
}
|
||||
|
||||
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 [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
|
||||
@@ -1097,19 +1084,27 @@ function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess
|
||||
|
||||
const getOperationLabel = () => {
|
||||
switch (operation) {
|
||||
case 'subscribe': return 'Subscribe';
|
||||
case 'unsubscribe': return 'Unsubscribe';
|
||||
case 'delete': return 'Delete';
|
||||
default: return 'Process';
|
||||
case 'subscribe':
|
||||
return 'Subscribe';
|
||||
case 'unsubscribe':
|
||||
return 'Unsubscribe';
|
||||
case 'delete':
|
||||
return 'Delete';
|
||||
default:
|
||||
return 'Process';
|
||||
}
|
||||
};
|
||||
|
||||
const getOperationColor = () => {
|
||||
switch (operation) {
|
||||
case 'subscribe': return 'green';
|
||||
case 'unsubscribe': return 'yellow';
|
||||
case 'delete': return 'red';
|
||||
default: return 'blue';
|
||||
case 'subscribe':
|
||||
return 'green';
|
||||
case 'unsubscribe':
|
||||
return 'yellow';
|
||||
case 'delete':
|
||||
return 'red';
|
||||
default:
|
||||
return 'blue';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1129,14 +1124,12 @@ function BulkActionsDialog({open, onOpenChange, operation, contactIds, onSuccess
|
||||
{contactIds.length !== 1 ? 's' : ''}?
|
||||
</p>
|
||||
{operation === 'delete' && (
|
||||
<p className="text-sm text-red-600 mt-2 font-medium">
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<p className="text-sm text-red-600 mt-2 font-medium">This action cannot be undone.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(status === 'processing') && (
|
||||
{status === 'processing' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<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" />
|
||||
<span className="font-medium">Operation failed</span>
|
||||
</div>
|
||||
<p className="text-sm text-red-800 mt-1">
|
||||
{errorMessage || 'Please try again.'}
|
||||
</p>
|
||||
<p className="text-sm text-red-800 mt-1">{errorMessage || 'Please try again.'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user