@@ -338,36 +338,88 @@ export default function FlywheelCampaigns() {
338338 } ;
339339
340340
341+ const renderCampaignActions = ( c : Campaign ) => (
342+ < >
343+ { c . status === 'DRAFT' && (
344+ < button onClick = { ( ) => handleDispatch ( c . id ) } disabled = { dispatching }
345+ className = "px-3 py-1.5 rounded-md text-xs font-semibold text-white transition-opacity flex items-center gap-1 disabled:opacity-60"
346+ style = { { background : brand . primaryColor } } >
347+ < IconSend size = { 12 } /> Launch
348+ </ button >
349+ ) }
350+ { c . status === 'ACTIVE' && (
351+ < button onClick = { ( ) => handlePause ( c . id , c . status ) } disabled = { mutating }
352+ className = "px-3 py-1.5 bg-amber-50 border border-amber-200 text-amber-700 rounded-md text-xs font-semibold hover:bg-amber-100 transition-colors flex items-center gap-1" >
353+ < IconPause size = { 12 } /> Pause
354+ </ button >
355+ ) }
356+ { c . status === 'PAUSED' && (
357+ < button onClick = { ( ) => handlePause ( c . id , c . status ) } disabled = { mutating }
358+ className = "px-3 py-1.5 rounded-md text-xs font-semibold text-white transition-opacity flex items-center gap-1"
359+ style = { { background : brand . primaryColor } } >
360+ < IconPlay size = { 12 } /> Resume
361+ </ button >
362+ ) }
363+ < button onClick = { ( ) => setSelectedCampaign ( c ) }
364+ className = "px-3 py-1.5 bg-white border border-slate-200 text-slate-600 rounded-md text-xs font-medium hover:border-slate-300 transition-colors flex items-center gap-1" >
365+ < IconEye size = { 12 } /> Details
366+ </ button >
367+ < button onClick = { ( ) => handleDelete ( c . id ) } disabled = { mutating }
368+ className = "px-3 py-1.5 bg-red-50 border border-red-200 text-red-600 rounded-md text-xs font-medium hover:bg-red-100 transition-colors flex items-center gap-1" >
369+ < IconX size = { 12 } /> Delete
370+ </ button >
371+ </ >
372+ ) ;
373+
374+ const summaryStats = [
375+ { label : 'Campaigns' , value : String ( campaigns . length ) , color : 'text-slate-900' } ,
376+ { label : 'Active' , value : String ( campaigns . filter ( c => c . status === 'ACTIVE' ) . length ) , color : 'text-emerald-600' } ,
377+ { label : 'Emails Sent' , value : campaigns . reduce ( ( s , c ) => s + ( c . stats ?. sent || 0 ) , 0 ) . toLocaleString ( ) , color : 'text-blue-600' } ,
378+ { label : 'Avg Open Rate' , value : ( ( ) => { const sent = campaigns . reduce ( ( s , c ) => s + ( c . stats ?. sent || 0 ) , 0 ) ; const op = campaigns . reduce ( ( s , c ) => s + ( c . stats ?. opens || 0 ) , 0 ) ; return sent > 0 ? `${ Math . round ( ( op / sent ) * 100 ) } %` : '—' ; } ) ( ) , color : 'text-amber-600' } ,
379+ ] ;
380+
341381 return (
342382 < AppShell >
343- < div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-8 pb-16" >
383+ < div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 sm:pt- 8 pb-16" >
344384
345385 { /* Header */ }
346- < div className = "flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4" >
347- < div >
348- < div className = "flex items-center gap-3 mb-1" >
349- < div className = "w-10 h-10 rounded-xl flex items-center justify-center" style = { { background : 'linear-gradient(135deg, #10b981, #059669)' } } >
350- < IconMail size = { 20 } style = { { color : '#fff' } } />
351- </ div >
352- < h1 className = "text-2xl font-bold tracking-tight text-slate-900" > Campaigns</ h1 >
386+ < div className = "flex flex-col md:flex-row md:justify-between md:items-center mb-6 gap-4" >
387+ < div className = "flex items-center gap-3" >
388+ < div className = "w-11 h-11 rounded-xl flex items-center justify-center shadow-sm flex-shrink-0" style = { { background : brand . gradient } } >
389+ < IconMail size = { 20 } style = { { color : '#fff' } } />
390+ </ div >
391+ < div >
392+ < h1 className = "text-xl sm:text-2xl font-bold tracking-tight text-slate-900" > Campaigns</ h1 >
393+ < p className = "text-sm text-slate-500" > Create, send, and track email marketing.</ p >
353394 </ div >
354- < p className = "text-slate-500 mt-1 ml-[52px]" > Create, manage, and track email marketing campaigns.</ p >
355395 </ div >
356- < div className = "flex items-center gap-3 " >
396+ < div className = "flex items-center gap-2 w-full md:w-auto " >
357397 < button
358398 onClick = { handleProcessNow }
359399 disabled = { processing }
360- className = "flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm border border-slate-200 bg-white text-slate-700 hover:bg-slate-50 transition-all disabled:opacity-60"
400+ className = "flex-1 md:flex-none flex items-center justify -center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm border border-slate-200 bg-white text-slate-700 hover:bg-slate-50 transition-all disabled:opacity-60"
361401 >
362402 < IconRefresh size = { 15 } className = { processing ? 'animate-spin' : '' } />
363- { processing ? 'Processing…' : 'Process Now' }
403+ < span className = "whitespace-nowrap" > { processing ? 'Processing…' : 'Process Now' } </ span >
364404 </ button >
365- < button onClick = { ( ) => { setWizardOpen ( true ) ; resetWizard ( ) ; } } className = "flex items-center gap-2 px-5 py-2.5 rounded-lg text-white font-medium text-sm shadow-md transition-all hover:shadow-lg hover:-translate-y-0.5 " style = { { background : brand . gradient } } >
366- < IconPlus size = { 16 } /> New Campaign
405+ < button onClick = { ( ) => { setWizardOpen ( true ) ; resetWizard ( ) ; } } className = "flex-1 md:flex-none flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg text-white font-medium text-sm shadow-md transition-all hover:shadow-lg" style = { { background : brand . gradient } } >
406+ < IconPlus size = { 16 } /> < span className = "whitespace-nowrap" > New Campaign</ span >
367407 </ button >
368408 </ div >
369409 </ div >
370410
411+ { /* Summary stats */ }
412+ { ! loading && campaigns . length > 0 && (
413+ < div className = "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6" >
414+ { summaryStats . map ( s => (
415+ < div key = { s . label } className = "bg-white border border-slate-200 rounded-xl px-4 py-3" >
416+ < p className = "text-xs text-slate-400 font-medium" > { s . label } </ p >
417+ < p className = { `text-lg sm:text-xl font-bold mt-0.5 ${ s . color } ` } > { s . value } </ p >
418+ </ div >
419+ ) ) }
420+ </ div >
421+ ) }
422+
371423 { processResult && (
372424 < div className = { `mb-6 px-5 py-3.5 rounded-xl border text-sm font-medium flex items-center justify-between ${
373425 processResult . failedCount > 0
@@ -387,8 +439,50 @@ export default function FlywheelCampaigns() {
387439 </ div >
388440 ) }
389441
390- { /* Campaign Table */ }
391- < div className = "card overflow-hidden" >
442+ { /* Empty state (shared) */ }
443+ { ! loading && campaigns . length === 0 && (
444+ < div className = "bg-white border border-slate-200 rounded-2xl px-6 py-16 text-center" >
445+ < div className = "inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4" style = { { background : brand . primaryLight , color : brand . primaryColor } } > < IconMail size = { 28 } /> </ div >
446+ < h3 className = "text-lg font-semibold text-slate-900 mb-1" > No campaigns yet</ h3 >
447+ < p className = "text-slate-500 text-sm mb-5 max-w-sm mx-auto" > Create your first campaign — start from a ready-made template or build your own.</ p >
448+ < button onClick = { ( ) => { setWizardOpen ( true ) ; resetWizard ( ) ; } } className = "px-5 py-2.5 text-white rounded-lg text-sm font-semibold inline-flex items-center gap-1.5" style = { { background : brand . primaryColor } } >
449+ < IconPlus size = { 15 } /> Create Campaign
450+ </ button >
451+ </ div >
452+ ) }
453+
454+ { /* ── Mobile / tablet: card list ── */ }
455+ < div className = "lg:hidden space-y-3" >
456+ { loading
457+ ? Array . from ( { length : 3 } ) . map ( ( _ , i ) => < div key = { i } className = "h-32 bg-slate-100 rounded-2xl animate-pulse" /> )
458+ : campaigns . map ( c => {
459+ const meta = STATUS_META [ c . status ] || STATUS_META . DRAFT ;
460+ return (
461+ < div key = { c . id } className = "bg-white border border-slate-200 rounded-2xl p-4 cursor-pointer hover:border-slate-300 transition-colors" onClick = { ( ) => setSelectedCampaign ( c ) } >
462+ < div className = "flex items-start justify-between gap-3 mb-3" >
463+ < div className = "min-w-0" >
464+ < p className = "font-semibold text-slate-900 truncate" > { c . name } </ p >
465+ < p className = "text-xs text-slate-400 mt-0.5" > { c . type . replace ( '_' , ' ' ) } · { new Date ( c . createdAt ) . toLocaleDateString ( ) } </ p >
466+ </ div >
467+ < span className = "flex-shrink-0 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold" style = { { background : meta . bg , color : meta . color } } > { meta . label } </ span >
468+ </ div >
469+ < div className = "grid grid-cols-4 gap-2 mb-3 text-center bg-slate-50 rounded-xl py-2.5" >
470+ < div > < p className = "text-[10px] text-slate-400 uppercase tracking-wide" > Audience</ p > < p className = "text-sm font-bold text-slate-700 mt-0.5" > { c . _count ?. leads || 0 } </ p > </ div >
471+ < div > < p className = "text-[10px] text-slate-400 uppercase tracking-wide" > Sent</ p > < p className = "text-sm font-bold text-blue-600 mt-0.5" > { c . stats ?. sent || 0 } </ p > </ div >
472+ < div > < p className = "text-[10px] text-slate-400 uppercase tracking-wide" > Opens</ p > < p className = "text-sm font-bold text-violet-600 mt-0.5" > { c . stats ?. opens || 0 } </ p > </ div >
473+ < div > < p className = "text-[10px] text-slate-400 uppercase tracking-wide" > Rate</ p > < p className = "text-sm font-bold text-amber-600 mt-0.5" > { c . stats ?. openRate || 0 } %</ p > </ div >
474+ </ div >
475+ < div className = "flex flex-wrap gap-1.5" onClick = { e => e . stopPropagation ( ) } >
476+ { renderCampaignActions ( c ) }
477+ </ div >
478+ </ div >
479+ ) ;
480+ } ) }
481+ </ div >
482+
483+ { /* ── Desktop: table ── */ }
484+ { ! ( ! loading && campaigns . length === 0 ) && (
485+ < div className = "hidden lg:block card overflow-hidden" >
392486 < div className = "overflow-x-auto" >
393487 < table className = "w-full text-left text-sm" >
394488 < thead className = "bg-slate-50/80 border-b border-slate-200 text-xs uppercase font-semibold text-slate-500" >
@@ -409,17 +503,6 @@ export default function FlywheelCampaigns() {
409503 Array . from ( { length : 5 } ) . map ( ( _ , i ) => (
410504 < tr key = { i } > < td colSpan = { 9 } className = "px-5 py-4" > < div className = "skeleton h-4 rounded" style = { { width : `${ 50 + Math . random ( ) * 40 } %` } } /> </ td > </ tr >
411505 ) )
412- ) : campaigns . length === 0 ? (
413- < tr >
414- < td colSpan = { 9 } className = "px-6 py-16 text-center" >
415- < div className = "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-50 text-emerald-300 mb-4 border border-emerald-100" > < IconMail size = { 28 } /> </ div >
416- < h3 className = "text-lg font-semibold text-slate-900 mb-1" > No campaigns yet</ h3 >
417- < p className = "text-slate-500 text-sm mb-4" > Create your first campaign to start engaging your audience.</ p >
418- < button onClick = { ( ) => { setWizardOpen ( true ) ; resetWizard ( ) ; } } className = "px-4 py-2 text-white rounded-lg text-sm font-medium" style = { { background : brand . primaryColor } } >
419- < IconPlus size = { 14 } className = "inline mr-1" /> Create Campaign
420- </ button >
421- </ td >
422- </ tr >
423506 ) : campaigns . map ( c => {
424507 const meta = STATUS_META [ c . status ] || STATUS_META . DRAFT ;
425508 return (
@@ -437,28 +520,8 @@ export default function FlywheelCampaigns() {
437520 < td className = "px-5 py-4 text-center font-semibold text-amber-600" > { c . stats ?. openRate || 0 } %</ td >
438521 < td className = "px-5 py-4 text-center text-xs text-slate-400" > { new Date ( c . createdAt ) . toLocaleDateString ( ) } </ td >
439522 < td className = "px-5 py-4 text-right" onClick = { e => e . stopPropagation ( ) } >
440- < div className = "flex justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity" >
441- { c . status === 'DRAFT' && (
442- < button onClick = { ( ) => handleDispatch ( c . id ) } disabled = { dispatching } className = "px-3 py-1.5 bg-emerald-50 border border-emerald-200 text-emerald-700 rounded-md text-xs font-semibold hover:bg-emerald-100 transition-colors flex items-center gap-1" >
443- < IconSend size = { 12 } /> Launch
444- </ button >
445- ) }
446- { c . status === 'ACTIVE' && (
447- < button onClick = { ( ) => handlePause ( c . id , c . status ) } disabled = { mutating } className = "px-3 py-1.5 bg-amber-50 border border-amber-200 text-amber-700 rounded-md text-xs font-semibold hover:bg-amber-100 transition-colors flex items-center gap-1" >
448- < IconPause size = { 12 } /> Pause
449- </ button >
450- ) }
451- { c . status === 'PAUSED' && (
452- < button onClick = { ( ) => handlePause ( c . id , c . status ) } disabled = { mutating } className = "px-3 py-1.5 bg-emerald-50 border border-emerald-200 text-emerald-700 rounded-md text-xs font-semibold hover:bg-emerald-100 transition-colors flex items-center gap-1" >
453- < IconPlay size = { 12 } /> Resume
454- </ button >
455- ) }
456- < button onClick = { ( ) => setSelectedCampaign ( c ) } className = "px-3 py-1.5 bg-white border border-slate-200 text-slate-600 rounded-md text-xs font-medium hover:text-blue-600 transition-colors flex items-center gap-1" >
457- < IconEye size = { 12 } /> Details
458- </ button >
459- < button onClick = { ( ) => handleDelete ( c . id ) } disabled = { mutating } className = "px-3 py-1.5 bg-red-50 border border-red-200 text-red-600 rounded-md text-xs font-medium hover:bg-red-100 transition-colors flex items-center gap-1" >
460- < IconX size = { 12 } /> Delete
461- </ button >
523+ < div className = "flex justify-end gap-1.5" >
524+ { renderCampaignActions ( c ) }
462525 </ div >
463526 </ td >
464527 </ tr >
@@ -468,6 +531,7 @@ export default function FlywheelCampaigns() {
468531 </ table >
469532 </ div >
470533 </ div >
534+ ) }
471535 </ div >
472536
473537 { /* ── CAMPAIGN WIZARD (Multi-step) ── */ }
@@ -563,7 +627,7 @@ export default function FlywheelCampaigns() {
563627 { leadSearchResults . map ( l => (
564628 < button key = { l . id } type = "button" onClick = { ( ) => { setPickedLeads ( p => [ ...p , l ] ) ; setLeadSearch ( '' ) ; setLeadSearchResults ( [ ] ) ; } }
565629 className = "w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-slate-50 transition-colors" >
566- < div className = "w-7 h-7 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center text-xs font-bold flex-shrink-0" >
630+ < div className = "w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0" style = { { background : brand . primaryLight , color : brand . primaryColor } } >
567631 { l . name [ 0 ] ?. toUpperCase ( ) }
568632 </ div >
569633 < div >
@@ -578,7 +642,7 @@ export default function FlywheelCampaigns() {
578642 < div className = "space-y-1.5" >
579643 < p className = "text-xs font-semibold text-slate-500 uppercase tracking-wide" > Selected ({ pickedLeads . length } )</ p >
580644 { pickedLeads . map ( l => (
581- < div key = { l . id } className = "flex items-center justify-between px-3 py-2 bg-emerald-50 border border-emerald-100 rounded-lg" >
645+ < div key = { l . id } className = "flex items-center justify-between px-3 py-2 bg-[#FBF8F3] border border-[#F0EAE0] rounded-lg" >
582646 < div >
583647 < span className = "text-sm font-semibold text-slate-800" > { l . name } </ span >
584648 < span className = "text-xs text-slate-400 ml-2" > { l . email } </ span >
0 commit comments