fix: Harmonize rings and hover states
This commit is contained in:
@@ -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-neutral-300' : ''}`}
|
className={`h-10 w-10 rounded-lg ${config.bgColor} flex items-center justify-center flex-shrink-0 ${isUpcoming ? 'border-2 border-dashed border-neutral-300' : ''}`}
|
||||||
>
|
>
|
||||||
<Icon className={`h-5 w-5 ${config.color}`} />
|
<Icon className={`h-5 w-5 ${config.color}`} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function ContactPicker({
|
|||||||
type="button"
|
type="button"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="flex h-10 w-full items-center justify-between rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-500 hover:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-0 transition-colors"
|
className="flex h-10 w-full items-center justify-between rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-500 hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors"
|
||||||
>
|
>
|
||||||
<span>{placeholder}</span>
|
<span>{placeholder}</span>
|
||||||
<ChevronsUpDown className="h-4 w-4 opacity-40 shrink-0" />
|
<ChevronsUpDown className="h-4 w-4 opacity-40 shrink-0" />
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
<div className="relative" ref={projectMenuRef}>
|
<div className="relative" ref={projectMenuRef}>
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleProjectMenu}
|
onClick={handleToggleProjectMenu}
|
||||||
className="w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-neutral-50 transition-colors"
|
className="w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-neutral-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<div className="h-8 w-8 rounded-lg bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
<div className="h-8 w-8 rounded-lg bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||||
@@ -194,7 +194,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
setActiveProject(project);
|
setActiveProject(project);
|
||||||
setShowProjectMenu(false);
|
setShowProjectMenu(false);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors whitespace-nowrap"
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<div className="h-6 w-6 rounded-md bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
<div className="h-6 w-6 rounded-md bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||||
{project.name.charAt(0).toUpperCase()}
|
{project.name.charAt(0).toUpperCase()}
|
||||||
@@ -209,7 +209,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
<Link
|
<Link
|
||||||
href="/projects/create"
|
href="/projects/create"
|
||||||
onClick={() => setShowProjectMenu(false)}
|
onClick={() => setShowProjectMenu(false)}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-neutral-700 whitespace-nowrap"
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-neutral-700 whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span>Create project</span>
|
<span>Create project</span>
|
||||||
@@ -238,7 +238,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setShowMobileMenu(false)}
|
onClick={() => setShowMobileMenu(false)}
|
||||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
||||||
isActive ? 'bg-neutral-100 text-neutral-900' : 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900'
|
isActive ? 'bg-neutral-100 text-neutral-900' : 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -258,7 +258,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
href={WIKI_URI}
|
href={WIKI_URI}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900"
|
className="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<BookOpen className="h-5 w-5" />
|
<BookOpen className="h-5 w-5" />
|
||||||
Documentation
|
Documentation
|
||||||
@@ -267,7 +267,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
<Link
|
<Link
|
||||||
href="/settings"
|
href="/settings"
|
||||||
onClick={() => setShowMobileMenu(false)}
|
onClick={() => setShowMobileMenu(false)}
|
||||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
||||||
router.pathname.startsWith('/settings')
|
router.pathname.startsWith('/settings')
|
||||||
? 'bg-neutral-100 text-neutral-900'
|
? 'bg-neutral-100 text-neutral-900'
|
||||||
: 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900'
|
: 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900'
|
||||||
@@ -280,7 +280,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
<div className="relative" ref={userMenuRef}>
|
<div className="relative" ref={userMenuRef}>
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleUserMenu}
|
onClick={handleToggleUserMenu}
|
||||||
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
|
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<div className="h-5 w-5 rounded-full bg-neutral-900 text-white flex items-center justify-center text-[10px] font-semibold flex-shrink-0">
|
<div className="h-5 w-5 rounded-full bg-neutral-900 text-white flex items-center justify-center text-[10px] font-semibold flex-shrink-0">
|
||||||
{user?.email?.charAt(0).toUpperCase() ?? '?'}
|
{user?.email?.charAt(0).toUpperCase() ?? '?'}
|
||||||
@@ -299,7 +299,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogoutClick}
|
onClick={handleLogoutClick}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-red-600"
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-red-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
<span>Log out</span>
|
<span>Log out</span>
|
||||||
@@ -340,7 +340,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
|||||||
<div className="lg:hidden h-16 bg-white border-b border-neutral-200 flex items-center px-4">
|
<div className="lg:hidden h-16 bg-white border-b border-neutral-200 flex items-center px-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowMobileMenu(true)}
|
onClick={() => setShowMobileMenu(true)}
|
||||||
className="p-2 rounded-lg hover:bg-neutral-100 transition-colors"
|
className="p-2 rounded-lg hover:bg-neutral-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<Menu className="h-6 w-6 text-neutral-900" />
|
<Menu className="h-6 w-6 text-neutral-900" />
|
||||||
|
|||||||
@@ -894,8 +894,8 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT
|
|||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
0 4px 6px -1px rgba(0, 0, 0, 0.07),
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
0 2px 4px -1px rgba(0, 0, 0, 0.04);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ export function Toolbar({editor, onInsertVariable, onInsertImage, canUploadImage
|
|||||||
</Button>
|
</Button>
|
||||||
{showColorPicker && (
|
{showColorPicker && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-10 left-0 bg-white border border-neutral-200 rounded-lg shadow-lg p-3 z-50 max-h-96 overflow-y-auto"
|
className="absolute top-10 left-0 bg-white border border-neutral-200 rounded-lg shadow-md p-3 z-50 max-h-96 overflow-y-auto"
|
||||||
style={{width: '280px'}}
|
style={{width: '280px'}}
|
||||||
>
|
>
|
||||||
{/* Custom color input */}
|
{/* Custom color input */}
|
||||||
@@ -483,7 +483,7 @@ export function Toolbar({editor, onInsertVariable, onInsertImage, canUploadImage
|
|||||||
>
|
>
|
||||||
{selectedColor === color && (
|
{selectedColor === color && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="w-2 h-2 rounded-full bg-white shadow-lg" />
|
<div className="w-2 h-2 rounded-full bg-white shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -511,7 +511,7 @@ export function Toolbar({editor, onInsertVariable, onInsertImage, canUploadImage
|
|||||||
<Link className="h-4 w-4" />
|
<Link className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{showLinkInput && (
|
{showLinkInput && (
|
||||||
<div className="absolute top-10 right-0 bg-white border border-neutral-200 rounded-lg shadow-lg p-2 z-50 min-w-max">
|
<div className="absolute top-10 right-0 bg-white border border-neutral-200 rounded-lg shadow-md p-2 z-50 min-w-max">
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
|
|||||||
@@ -36,22 +36,22 @@ function HelpResources() {
|
|||||||
<div className="mt-4 pt-4 border-t border-neutral-200">
|
<div className="mt-4 pt-4 border-t border-neutral-200">
|
||||||
<p className="text-xs font-medium text-neutral-500 mb-3">Need help?</p>
|
<p className="text-xs font-medium text-neutral-500 mb-3">Need help?</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<Link href={WIKI_URI} target="_blank" className="flex-1">
|
<Button asChild variant="outline" size="sm" className="flex-1">
|
||||||
<Button variant="outline" size="sm" className="w-full">
|
<Link href={WIKI_URI} target="_blank">
|
||||||
<BookOpen className="h-3.5 w-3.5" />
|
<BookOpen className="h-3.5 w-3.5" />
|
||||||
Documentation
|
Documentation
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
<Link href={`${LANDING_URI}/discord`} target="_blank" className="flex-1">
|
<Button asChild variant="outline" size="sm" className="flex-1">
|
||||||
<Button variant="outline" size="sm" className="w-full">
|
<Link href={`${LANDING_URI}/discord`} target="_blank">
|
||||||
<MessageCircle className="h-3.5 w-3.5" />
|
<MessageCircle className="h-3.5 w-3.5" />
|
||||||
Join Discord
|
Join Discord
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={copyEmail}
|
onClick={copyEmail}
|
||||||
whileTap={{scale: 0.97}}
|
whileTap={{scale: 0.97}}
|
||||||
className="flex-1 relative flex items-center justify-center gap-1.5 h-9 rounded-md border border-neutral-200 bg-white px-3 text-sm font-medium text-neutral-700 overflow-hidden transition-colors hover:bg-neutral-50 hover:text-neutral-900 hover:border-neutral-300"
|
className="flex-1 relative flex items-center justify-center gap-1.5 h-9 rounded-md border border-neutral-200 bg-white px-3 text-xs font-medium text-neutral-700 overflow-hidden transition-colors hover:bg-neutral-50 hover:text-neutral-900 hover:border-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
@@ -274,11 +274,9 @@ export function QuickStart({setupState, isLoading}: QuickStartProps) {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-neutral-600 leading-relaxed">{step.description}</p>
|
<p className="text-xs text-neutral-600 leading-relaxed">{step.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href={step.link} className="flex-shrink-0">
|
<Button asChild size="sm" variant={step.isCompleted ? 'outline' : 'default'} className="flex-shrink-0">
|
||||||
<Button size="sm" variant={step.isCompleted ? 'outline' : 'default'}>
|
<Link href={step.link}>{step.linkText}</Link>
|
||||||
{step.linkText}
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ export function SecurityWarningBanner({status}: SecurityWarningBannerProps) {
|
|||||||
: 'Your bounce or complaint rates are approaching limits. Review your contact lists and remove invalid addresses to maintain good standing.'}
|
: 'Your bounce or complaint rates are approaching limits. Review your contact lists and remove invalid addresses to maintain good standing.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/settings?tab=security">
|
<Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">
|
||||||
<Button size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">
|
<Link href="/settings?tab=security">View Details</Link>
|
||||||
View Details
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export function OnboardingBanner() {
|
|||||||
const Icon = step.icon;
|
const Icon = step.icon;
|
||||||
const isCurrent = step.id === currentStepId;
|
const isCurrent = step.id === currentStepId;
|
||||||
const base =
|
const base =
|
||||||
'group relative inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-900 focus-visible:ring-offset-2';
|
'group relative inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
||||||
const variant = step.done
|
const variant = step.done
|
||||||
? 'border-green-200 bg-green-50 text-green-900 hover:border-green-300'
|
? 'border-green-200 bg-green-50 text-green-900 hover:border-green-300'
|
||||||
: isCurrent
|
: isCurrent
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ export default function ResetPassword() {
|
|||||||
This link is invalid or has expired. Request a new one from the login page.
|
This link is invalid or has expired. Request a new one from the login page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/auth/login" className="mt-2">
|
<Button asChild className="mt-2">
|
||||||
<Button>Back to login</Button>
|
<Link href="/auth/login">Back to login</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -190,11 +190,9 @@ export default function VerifyEmail() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href="/auth/login">
|
<Button asChild variant="outline" className="w-full">
|
||||||
<Button variant="outline" className="w-full">
|
<Link href="/auth/login">Back to login</Link>
|
||||||
Back to login
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -273,11 +271,9 @@ export default function VerifyEmail() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href="/auth/login">
|
<Button asChild variant="outline" className="w-full">
|
||||||
<Button variant="outline" className="w-full">
|
<Link href="/auth/login">Back to login</Link>
|
||||||
Back to login
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -342,11 +342,9 @@ export default function CampaignDetailsPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
<Link href="/campaigns">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button type="button" variant="ghost" size="sm">
|
<Link href="/campaigns"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{c.name}</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{c.name}</h1>
|
||||||
@@ -848,11 +846,9 @@ export default function CampaignDetailsPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
<Link href="/campaigns">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href="/campaigns"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 sm:gap-3 mb-2">
|
<div className="flex items-center gap-2 sm:gap-3 mb-2">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{c.name}</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{c.name}</h1>
|
||||||
|
|||||||
@@ -177,11 +177,9 @@ export default function CreateCampaignPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
<Link href="/campaigns">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href="/campaigns"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Campaign</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Campaign</h1>
|
||||||
<p className="text-neutral-500 mt-1 text-sm sm:text-base">
|
<p className="text-neutral-500 mt-1 text-sm sm:text-base">
|
||||||
@@ -396,9 +394,9 @@ export default function CreateCampaignPage() {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<Link href="/campaigns">
|
<Button asChild variant="outline">
|
||||||
<Button type="button" variant="outline">Cancel</Button>
|
<Link href="/campaigns">Cancel</Link>
|
||||||
</Link>
|
</Button>
|
||||||
<Button type="submit" disabled={saving}>
|
<Button type="submit" disabled={saving}>
|
||||||
{saving ? 'Creating...' : 'Create Campaign'}
|
{saving ? 'Creating...' : 'Create Campaign'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -328,12 +328,12 @@ export default function CampaignsPage() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<Link href="/campaigns/create">
|
<Button asChild>
|
||||||
<Button>
|
<Link href="/campaigns/create">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Create Campaign
|
Create Campaign
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -347,10 +347,11 @@ export default function CampaignsPage() {
|
|||||||
const deliveryPct = campaign.totalRecipients > 0 ? (campaign.sentCount / campaign.totalRecipients) * 100 : 0;
|
const deliveryPct = campaign.totalRecipients > 0 ? (campaign.sentCount / campaign.totalRecipients) * 100 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={campaign.id} className="transition-colors hover:border-neutral-300 flex flex-col">
|
<Card key={campaign.id} className="transition-colors hover:border-neutral-300 flex flex-col [&:has([data-card-link]:focus-visible)]:ring-2 [&:has([data-card-link]:focus-visible)]:ring-ring [&:has([data-card-link]:focus-visible)]:ring-offset-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/campaigns/${campaign.id}`}
|
href={`/campaigns/${campaign.id}`}
|
||||||
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl"
|
data-card-link=""
|
||||||
|
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl focus-visible:outline-none"
|
||||||
aria-label={`Open ${campaign.name}`}
|
aria-label={`Open ${campaign.name}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
@@ -432,17 +433,15 @@ export default function CampaignsPage() {
|
|||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<span>Updated {formatRelativeTime(campaign.updatedAt)}</span>
|
<span>Updated {formatRelativeTime(campaign.updatedAt)}</span>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(campaign.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(campaign.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link href={`/campaigns/${campaign.id}`} aria-label={campaign.status === 'DRAFT' ? 'Edit campaign' : 'View campaign'}>
|
<Button asChild variant="ghost" size="sm" title={campaign.status === 'DRAFT' ? 'Edit campaign' : 'View campaign'}>
|
||||||
<Button variant="ghost" size="sm" title={campaign.status === 'DRAFT' ? 'Edit campaign' : 'View campaign'}>
|
<Link href={`/campaigns/${campaign.id}`} aria-label={campaign.status === 'DRAFT' ? 'Edit campaign' : 'View campaign'}><Edit className="h-4 w-4" /></Link>
|
||||||
<Edit className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button variant="ghost" size="sm" title="Duplicate campaign" onClick={() => handleDuplicate(campaign.id)}>
|
<Button variant="ghost" size="sm" title="Duplicate campaign" onClick={() => handleDuplicate(campaign.id)}>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -105,12 +105,12 @@ export default function ContactDetailPage() {
|
|||||||
<p className="text-neutral-500 mb-6">
|
<p className="text-neutral-500 mb-6">
|
||||||
The contact you're looking for doesn't exist or has been deleted.
|
The contact you're looking for doesn't exist or has been deleted.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/contacts">
|
<Button asChild>
|
||||||
<Button>
|
<Link href="/contacts">
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back to Contacts
|
Back to Contacts
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
@@ -124,11 +124,9 @@ export default function ContactDetailPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex items-center gap-3 sm:gap-4 min-w-0">
|
<div className="flex items-center gap-3 sm:gap-4 min-w-0">
|
||||||
<Link href="/contacts">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Button variant="outline" size="sm">
|
<Link href="/contacts"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{contact.email}</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{contact.email}</h1>
|
||||||
<p className="mt-1">
|
<p className="mt-1">
|
||||||
@@ -261,7 +259,7 @@ export default function ContactDetailPage() {
|
|||||||
<p className="text-sm font-medium text-neutral-900">Created</p>
|
<p className="text-sm font-medium text-neutral-900">Created</p>
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<p className="text-sm text-neutral-500">{dayjs(contact.createdAt).fromNow()}</p>
|
<p className="text-sm text-neutral-500">{dayjs(contact.createdAt).fromNow()}</p>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(contact.createdAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(contact.createdAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +269,7 @@ export default function ContactDetailPage() {
|
|||||||
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
|
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<p className="text-sm text-neutral-500">{dayjs(contact.updatedAt).fromNow()}</p>
|
<p className="text-sm text-neutral-500">{dayjs(contact.updatedAt).fromNow()}</p>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(contact.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(contact.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -338,18 +338,16 @@ export default function ContactsPage() {
|
|||||||
<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">
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
{formatRelativeTime(contact.createdAt)}
|
{formatRelativeTime(contact.createdAt)}
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-1/2 transform -translate-x-1/2 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-1/2 transform -translate-x-1/2 mb-1 whitespace-nowrap">
|
||||||
{dayjs(contact.createdAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(contact.createdAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Link href={`/contacts/${contact.id}`}>
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href={`/contacts/${contact.id}`}><Edit className="h-4 w-4" /></Link>
|
||||||
<Edit className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => promptDelete(contact.id)}>
|
<Button variant="ghost" size="sm" onClick={() => promptDelete(contact.id)}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -388,16 +386,14 @@ export default function ContactsPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<span className="text-xs text-neutral-500">{formatRelativeTime(contact.createdAt)}</span>
|
<span className="text-xs text-neutral-500">{formatRelativeTime(contact.createdAt)}</span>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(contact.createdAt).format('Do MMMM YYYY, h:mm A')}
|
{dayjs(contact.createdAt).format('Do MMMM YYYY, h:mm A')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link href={`/contacts/${contact.id}`}>
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href={`/contacts/${contact.id}`}><Edit className="h-4 w-4" /></Link>
|
||||||
<Edit className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => promptDelete(contact.id)}>
|
<Button variant="ghost" size="sm" onClick={() => promptDelete(contact.id)}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -113,11 +113,9 @@ export default function Index() {
|
|||||||
Please contact support to resolve this issue and get your project re-enabled.
|
Please contact support to resolve this issue and get your project re-enabled.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/settings?tab=security">
|
<Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">
|
||||||
<Button size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">
|
<Link href="/settings?tab=security">View Details</Link>
|
||||||
View Details
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -167,11 +165,9 @@ export default function Index() {
|
|||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
Your emails currently include Plunk branding. Upgrade to a subscription to remove it.
|
Your emails currently include Plunk branding. Upgrade to a subscription to remove it.
|
||||||
</span>
|
</span>
|
||||||
<Link href="/settings?tab=billing">
|
<Button asChild size="sm" className="w-full sm:w-auto">
|
||||||
<Button size="sm" className="w-full sm:w-auto">
|
<Link href="/settings?tab=billing">Upgrade Now</Link>
|
||||||
Upgrade Now
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export default function Manage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => void handleToggleSubscription()}
|
onClick={() => void handleToggleSubscription()}
|
||||||
disabled={updating}
|
disabled={updating}
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2 ${
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
||||||
contact?.subscribed ? 'bg-neutral-900' : 'bg-neutral-200'
|
contact?.subscribed ? 'bg-neutral-900' : 'bg-neutral-200'
|
||||||
} ${updating ? 'opacity-50 cursor-not-allowed' : ''}`}
|
} ${updating ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function OnboardingWelcome() {
|
|||||||
transition={{duration: 0.35, delay: 0.08 + index * 0.06, ease: [0.22, 1, 0.36, 1]}}
|
transition={{duration: 0.35, delay: 0.08 + index * 0.06, ease: [0.22, 1, 0.36, 1]}}
|
||||||
whileHover={{y: -2}}
|
whileHover={{y: -2}}
|
||||||
whileTap={{scale: 0.995}}
|
whileTap={{scale: 0.995}}
|
||||||
className="group flex flex-col gap-5 rounded-xl border border-neutral-200 bg-white p-6 text-left shadow-sm transition-colors hover:border-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-900 focus-visible:ring-offset-2"
|
className="group flex flex-col gap-5 rounded-xl border border-neutral-200 bg-white p-6 text-left shadow-sm transition-colors hover:border-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="flex h-10 w-10 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-900">
|
<span className="flex h-10 w-10 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-900">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
EmptyState,
|
||||||
IconSpinner,
|
IconSpinner,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
@@ -194,12 +195,12 @@ export default function SegmentDetailPage() {
|
|||||||
<p className="text-neutral-500 mb-6">
|
<p className="text-neutral-500 mb-6">
|
||||||
The segment you're looking for doesn't exist or has been deleted.
|
The segment you're looking for doesn't exist or has been deleted.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/segments">
|
<Button asChild>
|
||||||
<Button>
|
<Link href="/segments">
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back to Segments
|
Back to Segments
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
@@ -213,11 +214,9 @@ export default function SegmentDetailPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/segments">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Button variant="outline" size="sm">
|
<Link href="/segments"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-2xl sm: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>
|
||||||
@@ -276,7 +275,7 @@ export default function SegmentDetailPage() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={trackMembership}
|
checked={trackMembership}
|
||||||
onChange={e => setTrackMembership(e.target.checked)}
|
onChange={e => setTrackMembership(e.target.checked)}
|
||||||
className="mt-1 h-4 w-4 text-neutral-900 focus:ring-neutral-900 border-neutral-300 rounded"
|
className="mt-1 h-4 w-4 text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 border-neutral-300 rounded"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="trackMembership" className="font-medium cursor-pointer">
|
<Label htmlFor="trackMembership" className="font-medium cursor-pointer">
|
||||||
@@ -365,12 +364,11 @@ export default function SegmentDetailPage() {
|
|||||||
<p className="text-sm text-neutral-500">Loading contacts...</p>
|
<p className="text-sm text-neutral-500">Loading contacts...</p>
|
||||||
</div>
|
</div>
|
||||||
) : contactsData?.data.length === 0 ? (
|
) : contactsData?.data.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<EmptyState
|
||||||
<Users className="h-12 w-12 text-neutral-400 mx-auto mb-4" />
|
icon={Users}
|
||||||
<p className="text-neutral-500">
|
title={isStatic ? 'No members yet' : 'No contacts match'}
|
||||||
{isStatic ? 'No members in this segment yet' : 'No contacts match this segment'}
|
description={isStatic ? 'Add contacts to this segment to get started.' : 'No contacts currently match these filter conditions.'}
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -385,11 +383,9 @@ export default function SegmentDetailPage() {
|
|||||||
<span className="text-sm font-medium">{contact.email}</span>
|
<span className="text-sm font-medium">{contact.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href={`/contacts/${contact.id}`}>
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href={`/contacts/${contact.id}`}>View</Link>
|
||||||
View
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
{isStatic && (
|
{isStatic && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructiveGhost"
|
variant="destructiveGhost"
|
||||||
@@ -494,7 +490,7 @@ export default function SegmentDetailPage() {
|
|||||||
<p className="text-sm font-medium text-neutral-900">Created</p>
|
<p className="text-sm font-medium text-neutral-900">Created</p>
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<p className="text-sm text-neutral-500">{dayjs(segment.createdAt).fromNow()}</p>
|
<p className="text-sm text-neutral-500">{dayjs(segment.createdAt).fromNow()}</p>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(segment.createdAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(segment.createdAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,7 +500,7 @@ export default function SegmentDetailPage() {
|
|||||||
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
|
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<p className="text-sm text-neutral-500">{dayjs(segment.updatedAt).fromNow()}</p>
|
<p className="text-sm text-neutral-500">{dayjs(segment.updatedAt).fromNow()}</p>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(segment.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(segment.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ export default function SegmentsPage() {
|
|||||||
Create dynamic audience groups based on contact attributes and behaviors
|
Create dynamic audience groups based on contact attributes and behaviors
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/segments/new" className="w-full sm:w-auto">
|
<Button asChild className="w-full sm:w-auto">
|
||||||
<Button className="w-full sm:w-auto">
|
<Link href="/segments/new">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Create Segment</span>
|
<span className="hidden sm:inline">Create Segment</span>
|
||||||
<span className="sm:hidden">Create</span>
|
<span className="sm:hidden">Create</span>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
@@ -145,12 +145,12 @@ export default function SegmentsPage() {
|
|||||||
description={searchInput ? 'Try a different search term.' : 'Group contacts by attributes to target specific audiences.'}
|
description={searchInput ? 'Try a different search term.' : 'Group contacts by attributes to target specific audiences.'}
|
||||||
action={
|
action={
|
||||||
!searchInput ? (
|
!searchInput ? (
|
||||||
<Link href="/segments/new">
|
<Button asChild>
|
||||||
<Button>
|
<Link href="/segments/new">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Create Segment
|
Create Segment
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -162,10 +162,11 @@ export default function SegmentsPage() {
|
|||||||
const isDynamic = (segment as unknown as {type: string}).type !== 'STATIC';
|
const isDynamic = (segment as unknown as {type: string}).type !== 'STATIC';
|
||||||
const filterCount = isDynamic ? countFiltersInCondition(segment.condition) : 0;
|
const filterCount = isDynamic ? countFiltersInCondition(segment.condition) : 0;
|
||||||
return (
|
return (
|
||||||
<Card key={segment.id} className="transition-colors hover:border-neutral-300 flex flex-col">
|
<Card key={segment.id} className="transition-colors hover:border-neutral-300 flex flex-col [&:has([data-card-link]:focus-visible)]:ring-2 [&:has([data-card-link]:focus-visible)]:ring-ring [&:has([data-card-link]:focus-visible)]:ring-offset-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/segments/${segment.id}`}
|
href={`/segments/${segment.id}`}
|
||||||
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl"
|
data-card-link=""
|
||||||
|
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl focus-visible:outline-none"
|
||||||
aria-label={`Open ${segment.name}`}
|
aria-label={`Open ${segment.name}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
@@ -195,17 +196,15 @@ export default function SegmentsPage() {
|
|||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<span>Updated {formatRelativeTime(segment.updatedAt)}</span>
|
<span>Updated {formatRelativeTime(segment.updatedAt)}</span>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(segment.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(segment.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link href={`/segments/${segment.id}`} aria-label="Edit segment">
|
<Button asChild variant="ghost" size="sm" title="Edit segment">
|
||||||
<Button variant="ghost" size="sm" title="Edit segment">
|
<Link href={`/segments/${segment.id}`} aria-label="Edit segment"><Edit className="h-4 w-4" /></Link>
|
||||||
<Edit className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -79,11 +79,9 @@ export default function NewSegmentPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/segments">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Button variant="outline" size="sm">
|
<Link href="/segments"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl sm: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">
|
||||||
@@ -159,7 +157,7 @@ export default function NewSegmentPage() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={trackMembership}
|
checked={trackMembership}
|
||||||
onChange={e => setTrackMembership(e.target.checked)}
|
onChange={e => setTrackMembership(e.target.checked)}
|
||||||
className="mt-1 h-4 w-4 text-neutral-900 focus:ring-neutral-900 border-neutral-300 rounded"
|
className="mt-1 h-4 w-4 text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 border-neutral-300 rounded"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="trackMembership" className="font-medium cursor-pointer">
|
<Label htmlFor="trackMembership" className="font-medium cursor-pointer">
|
||||||
@@ -204,11 +202,9 @@ export default function NewSegmentPage() {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Link href="/segments">
|
<Button asChild variant="outline" disabled={isSubmitting}>
|
||||||
<Button type="button" variant="outline" disabled={isSubmitting}>
|
<Link href="/segments">Cancel</Link>
|
||||||
Cancel
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
<Save className="h-4 w-4 mr-2" />
|
<Save className="h-4 w-4 mr-2" />
|
||||||
{isSubmitting ? 'Creating...' : 'Create Segment'}
|
{isSubmitting ? 'Creating...' : 'Create Segment'}
|
||||||
|
|||||||
@@ -130,11 +130,9 @@ export default function TemplateEditorPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
<Link href="/templates">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button type="button" variant="ghost" size="sm">
|
<Link href="/templates"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Edit Template</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Edit Template</h1>
|
||||||
<p className="text-neutral-500 mt-1 text-sm sm:text-base">Make changes to your email template</p>
|
<p className="text-neutral-500 mt-1 text-sm sm:text-base">Make changes to your email template</p>
|
||||||
|
|||||||
@@ -73,11 +73,9 @@ export default function CreateTemplatePage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
<Link href="/templates">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href="/templates"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Template</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900">Create Template</h1>
|
||||||
<p className="text-neutral-500 mt-1 text-sm sm:text-base">
|
<p className="text-neutral-500 mt-1 text-sm sm:text-base">
|
||||||
@@ -217,9 +215,9 @@ export default function CreateTemplatePage() {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<Link href="/templates">
|
<Button asChild variant="outline">
|
||||||
<Button type="button" variant="outline">Cancel</Button>
|
<Link href="/templates">Cancel</Link>
|
||||||
</Link>
|
</Button>
|
||||||
<Button type="submit" disabled={saving}>
|
<Button type="submit" disabled={saving}>
|
||||||
{saving ? 'Creating...' : 'Create Template'}
|
{saving ? 'Creating...' : 'Create Template'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ export default function TemplatesPage() {
|
|||||||
{data?.total ? `${data.total} total templates` : ''}
|
{data?.total ? `${data.total} total templates` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/templates/create" className="w-full sm:w-auto">
|
<Button asChild className="w-full sm:w-auto">
|
||||||
<Button className="w-full sm:w-auto">
|
<Link href="/templates/create">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Create Template</span>
|
<span className="hidden sm:inline">Create Template</span>
|
||||||
<span className="sm:hidden">Create</span>
|
<span className="sm:hidden">Create</span>
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search & Filters */}
|
{/* Search & Filters */}
|
||||||
@@ -149,12 +149,12 @@ export default function TemplatesPage() {
|
|||||||
description={search ? 'Try a different search term.' : 'Create reusable email designs for campaigns.'}
|
description={search ? 'Try a different search term.' : 'Create reusable email designs for campaigns.'}
|
||||||
action={
|
action={
|
||||||
!search ? (
|
!search ? (
|
||||||
<Link href="/templates/create">
|
<Button asChild>
|
||||||
<Button>
|
<Link href="/templates/create">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Create Template
|
Create Template
|
||||||
</Button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -164,10 +164,11 @@ export default function TemplatesPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{data?.data.map(template => (
|
{data?.data.map(template => (
|
||||||
<Card key={template.id} className="transition-colors hover:border-neutral-300 flex flex-col">
|
<Card key={template.id} className="transition-colors hover:border-neutral-300 flex flex-col [&:has([data-card-link]:focus-visible)]:ring-2 [&:has([data-card-link]:focus-visible)]:ring-ring [&:has([data-card-link]:focus-visible)]:ring-offset-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/templates/${template.id}`}
|
href={`/templates/${template.id}`}
|
||||||
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl"
|
data-card-link=""
|
||||||
|
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl focus-visible:outline-none"
|
||||||
aria-label={`Edit ${template.name}`}
|
aria-label={`Edit ${template.name}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
@@ -186,17 +187,15 @@ export default function TemplatesPage() {
|
|||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<span>Updated {formatRelativeTime(template.updatedAt)}</span>
|
<span>Updated {formatRelativeTime(template.updatedAt)}</span>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(template.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(template.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link href={`/templates/${template.id}`} aria-label="Edit template">
|
<Button asChild variant="ghost" size="sm" title="Edit template">
|
||||||
<Button variant="ghost" size="sm" title="Edit template">
|
<Link href={`/templates/${template.id}`} aria-label="Edit template"><Edit className="h-4 w-4" /></Link>
|
||||||
<Edit className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button variant="ghost" size="sm" title="Duplicate template" onClick={() => handleDuplicate(template.id)}>
|
<Button variant="ghost" size="sm" title="Duplicate template" onClick={() => handleDuplicate(template.id)}>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -443,11 +443,9 @@ export default function WorkflowEditorPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
<Link href="/workflows">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Button variant="ghost" size="sm">
|
<Link href="/workflows"><ArrowLeft className="h-4 w-4" /></Link>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{workflow.name}</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-neutral-900 truncate">{workflow.name}</h1>
|
||||||
@@ -573,7 +571,7 @@ export default function WorkflowEditorPage() {
|
|||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('builder')}
|
onClick={() => setActiveTab('builder')}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
||||||
activeTab === 'builder'
|
activeTab === 'builder'
|
||||||
? 'border-neutral-900 text-neutral-900'
|
? 'border-neutral-900 text-neutral-900'
|
||||||
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
|
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
|
||||||
@@ -583,7 +581,7 @@ export default function WorkflowEditorPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('executions')}
|
onClick={() => setActiveTab('executions')}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
className={`py-2 px-1 border-b-2 font-medium text-sm rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
||||||
activeTab === 'executions'
|
activeTab === 'executions'
|
||||||
? 'border-neutral-900 text-neutral-900'
|
? 'border-neutral-900 text-neutral-900'
|
||||||
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
|
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
|
||||||
@@ -682,7 +680,7 @@ export default function WorkflowEditorPage() {
|
|||||||
<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">
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
{dayjs(execution.startedAt).fromNow()}
|
{dayjs(execution.startedAt).fromNow()}
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-1/2 transform -translate-x-1/2 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-1/2 transform -translate-x-1/2 mb-1 whitespace-nowrap">
|
||||||
{dayjs(execution.startedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(execution.startedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -157,10 +157,11 @@ export default function WorkflowsPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{data?.data.map(workflow => (
|
{data?.data.map(workflow => (
|
||||||
<Card key={workflow.id} className="transition-colors hover:border-neutral-300 flex flex-col">
|
<Card key={workflow.id} className="transition-colors hover:border-neutral-300 flex flex-col [&:has([data-card-link]:focus-visible)]:ring-2 [&:has([data-card-link]:focus-visible)]:ring-ring [&:has([data-card-link]:focus-visible)]:ring-offset-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/workflows/${workflow.id}`}
|
href={`/workflows/${workflow.id}`}
|
||||||
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl"
|
data-card-link=""
|
||||||
|
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl focus-visible:outline-none"
|
||||||
aria-label={`Open ${workflow.name}`}
|
aria-label={`Open ${workflow.name}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
@@ -201,7 +202,7 @@ export default function WorkflowsPage() {
|
|||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
<div className="group relative inline-block cursor-help">
|
<div className="group relative inline-block cursor-help">
|
||||||
<span>Updated {formatRelativeTime(workflow.updatedAt)}</span>
|
<span>Updated {formatRelativeTime(workflow.updatedAt)}</span>
|
||||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-md bottom-full left-0 mb-1 whitespace-nowrap">
|
||||||
{dayjs(workflow.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
{dayjs(workflow.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,11 +216,9 @@ export default function WorkflowsPage() {
|
|||||||
>
|
>
|
||||||
{workflow.enabled ? <PowerOff className="h-4 w-4" /> : <Power className="h-4 w-4" />}
|
{workflow.enabled ? <PowerOff className="h-4 w-4" /> : <Power className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
<Link href={`/workflows/${workflow.id}`} aria-label="Edit workflow">
|
<Button asChild variant="ghost" size="sm" title="Edit workflow">
|
||||||
<Button variant="ghost" size="sm" title="Edit workflow">
|
<Link href={`/workflows/${workflow.id}`} aria-label="Edit workflow"><Edit className="h-4 w-4" /></Link>
|
||||||
<Edit className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -359,7 +358,7 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
|
|||||||
value={description}
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={e => setDescription(e.target.value)}
|
||||||
placeholder="Send a series of welcome emails to new subscribers"
|
placeholder="Send a series of welcome emails to new subscribers"
|
||||||
className="w-full px-3 py-2 border border-neutral-200 rounded-md text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
className="w-full px-3 py-2 border border-neutral-200 rounded-md text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,7 +432,7 @@ function CreateWorkflowDialog({open, onOpenChange, onSuccess}: CreateWorkflowDia
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={allowReentry}
|
checked={allowReentry}
|
||||||
onChange={e => setAllowReentry(e.target.checked)}
|
onChange={e => setAllowReentry(e.target.checked)}
|
||||||
className="mt-0.5 h-4 w-4 text-neutral-900 focus:ring-neutral-900 border-neutral-300 rounded"
|
className="mt-0.5 h-4 w-4 text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 border-neutral-300 rounded"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label htmlFor="allowReentry" className="font-medium cursor-pointer">
|
<Label htmlFor="allowReentry" className="font-medium cursor-pointer">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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-1 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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|||||||
import {cn} from '../../lib';
|
import {cn} from '../../lib';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer',
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const ChartTooltipContent = React.forwardRef<HTMLDivElement, ChartTooltipContent
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
|
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-md',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<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">
|
<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-2 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>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-full rounded-md border border-neutral-200 bg-transparent px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
'flex h-9 w-full rounded-md border border-neutral-200 bg-transparent px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -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-visible:outline-none focus-visible:ring-1 focus-visible: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-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const Switch = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
|
'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({classNam
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-h-[80px] w-full 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-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'flex min-h-[80px] w-full 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-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
Reference in New Issue
Block a user