Fix build errors

This commit is contained in:
Dries Augustyns
2025-12-04 11:42:16 +01:00
parent 6fe3779fcb
commit fde00ebda3
10 changed files with 177 additions and 193 deletions
+122 -122
View File
@@ -132,135 +132,135 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
}, [handleLogout]); }, [handleLogout]);
// Sidebar content (reusable for both desktop and mobile) // Sidebar content (reusable for both desktop and mobile)
const SidebarContent = () => ( const SidebarContent = () => (
<> <>
{/* Logo */} {/* Logo */}
<div className="h-16 flex items-center gap-2 px-6 border-b border-neutral-200"> <div className="h-16 flex items-center gap-2 px-6 border-b border-neutral-200">
<Image src="/assets/logo.png" alt="Plunk" width={28} height={28} className="rounded" /> <Image src="/assets/logo.png" alt="Plunk" width={28} height={28} className="rounded" />
<h1 className="text-xl font-bold text-neutral-900">Plunk</h1> <h1 className="text-xl font-bold text-neutral-900">Plunk</h1>
</div> </div>
{/* Project Switcher */} {/* Project Switcher */}
<div className="p-4 border-b border-neutral-200"> <div className="p-4 border-b border-neutral-200">
<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"
> >
<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">
{activeProject?.name.charAt(0).toUpperCase() || 'P'} {activeProject?.name.charAt(0).toUpperCase() || 'P'}
</div>
<span className="font-medium text-neutral-900 truncate">{activeProject?.name || 'Select project'}</span>
</div> </div>
<span className="font-medium text-neutral-900 truncate">{activeProject?.name || 'Select project'}</span> <ChevronDown className="h-4 w-4 text-neutral-500 flex-shrink-0" />
</div> </button>
<ChevronDown className="h-4 w-4 text-neutral-500 flex-shrink-0" />
</button>
{/* Project Dropdown */} {/* Project Dropdown */}
{showProjectMenu && ( {showProjectMenu && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1"> <div className="absolute top-full left-0 right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
{availableProjects.map(project => ( {availableProjects.map(project => (
<button <button
key={project.id} key={project.id}
onClick={() => { onClick={() => {
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" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors"
>
<div className="h-6 w-6 rounded bg-neutral-900 text-white flex items-center justify-center text-xs font-medium">
{project.name.charAt(0).toUpperCase()}
</div>
<span className="text-neutral-900">{project.name}</span>
{activeProject?.id === project.id && (
<div className="ml-auto h-1.5 w-1.5 rounded-full bg-neutral-900" />
)}
</button>
))}
<div className="border-t border-neutral-200 my-1" />
<Link
href="/projects/create"
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-neutral-700"
>
<Plus className="h-4 w-4" />
<span>Create project</span>
</Link>
</div>
)}
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 overflow-y-auto">
{navigation.map((section, sectionIndex) => (
<div key={sectionIndex} className={sectionIndex > 0 ? 'mt-6' : ''}>
{section.title && (
<p className="px-3 mb-2 text-xs font-semibold text-neutral-500 uppercase tracking-wider">
{section.title}
</p>
)}
<div className="space-y-1">
{section.items.map(item => {
const isActive = router.pathname === item.href;
const Icon = item.icon;
return (
<Link
key={item.name}
href={item.href}
onClick={() => setShowMobileMenu(false)}
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
isActive ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
}`}
> >
<Icon className="h-5 w-5" /> <div className="h-6 w-6 rounded bg-neutral-900 text-white flex items-center justify-center text-xs font-medium">
{item.name} {project.name.charAt(0).toUpperCase()}
</Link> </div>
); <span className="text-neutral-900">{project.name}</span>
})} {activeProject?.id === project.id && (
</div> <div className="ml-auto h-1.5 w-1.5 rounded-full bg-neutral-900" />
)}
</button>
))}
<div className="border-t border-neutral-200 my-1" />
<Link
href="/projects/create"
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-neutral-700"
>
<Plus className="h-4 w-4" />
<span>Create project</span>
</Link>
</div>
)}
</div> </div>
))}
</nav>
{/* Settings & User Menu */}
<div className="border-t border-neutral-200 p-3 space-y-1">
<Link
href="/settings"
onClick={() => setShowMobileMenu(false)}
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
router.pathname.startsWith('/settings') ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
}`}
>
<Settings className="h-5 w-5" />
Settings
</Link>
<div className="relative" ref={userMenuRef}>
<button
onClick={handleToggleUserMenu}
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
>
<User className="h-5 w-5" />
<span className="flex-1 text-left truncate">{user?.email}</span>
<ChevronDown className="h-4 w-4 text-neutral-500" />
</button>
{/* User Dropdown */}
{showUserMenu && (
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
<button
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"
>
<LogOut className="h-4 w-4" />
<span>Log out</span>
</button>
</div>
)}
</div> </div>
</div>
</> {/* Navigation */}
); <nav className="flex-1 px-3 py-4 overflow-y-auto">
{navigation.map((section, sectionIndex) => (
<div key={sectionIndex} className={sectionIndex > 0 ? 'mt-6' : ''}>
{section.title && (
<p className="px-3 mb-2 text-xs font-semibold text-neutral-500 uppercase tracking-wider">
{section.title}
</p>
)}
<div className="space-y-1">
{section.items.map(item => {
const isActive = router.pathname === item.href;
const Icon = item.icon;
return (
<Link
key={item.name}
href={item.href}
onClick={() => setShowMobileMenu(false)}
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
isActive ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
}`}
>
<Icon className="h-5 w-5" />
{item.name}
</Link>
);
})}
</div>
</div>
))}
</nav>
{/* Settings & User Menu */}
<div className="border-t border-neutral-200 p-3 space-y-1">
<Link
href="/settings"
onClick={() => setShowMobileMenu(false)}
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
router.pathname.startsWith('/settings') ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
}`}
>
<Settings className="h-5 w-5" />
Settings
</Link>
<div className="relative" ref={userMenuRef}>
<button
onClick={handleToggleUserMenu}
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
>
<User className="h-5 w-5" />
<span className="flex-1 text-left truncate">{user?.email}</span>
<ChevronDown className="h-4 w-4 text-neutral-500" />
</button>
{/* User Dropdown */}
{showUserMenu && (
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
<button
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"
>
<LogOut className="h-4 w-4" />
<span>Log out</span>
</button>
</div>
)}
</div>
</div>
</>
);
return ( return (
<div className="flex h-screen bg-neutral-50"> <div className="flex h-screen bg-neutral-50">
@@ -35,7 +35,6 @@ interface EmailEditorProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
placeholder?: string; placeholder?: string;
canUploadImages?: boolean;
// Optional props for preview header // Optional props for preview header
subject?: string; subject?: string;
from?: string; from?: string;
@@ -119,6 +119,44 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
// Helper to get default value based on field type
const getDefaultValueForType = useCallback((type: string) => {
switch (type) {
case 'boolean':
return true;
case 'number':
return 0;
case 'date':
return '';
default:
return '';
}
}, []);
// Helper to get valid operators for a field type
const getOperatorsForType = useCallback((type: string, isEvent: boolean) => {
if (isEvent) {
return EVENT_OPERATORS;
}
if (type === 'boolean') {
return STANDARD_OPERATORS.filter(op =>
['equals', 'notEquals', 'exists', 'notExists'].includes(op.value)
);
}
if (type === 'number' || type === 'date') {
return STANDARD_OPERATORS.filter(op =>
['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'exists', 'notExists', 'within'].includes(op.value)
);
}
// String type - no within operator, no comparison operators
return STANDARD_OPERATORS.filter(op =>
['equals', 'notEquals', 'contains', 'notContains', 'exists', 'notExists'].includes(op.value)
);
}, []);
const needsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(filter.operator); const needsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(filter.operator);
const needsUnit = ['within', 'triggeredWithin'].includes(filter.operator); const needsUnit = ['within', 'triggeredWithin'].includes(filter.operator);
@@ -208,45 +246,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
setOpen(false); setOpen(false);
setSearch(''); setSearch('');
}, [availableFields, filter.operator, fieldType, onChange]); }, [availableFields, filter.operator, fieldType, onChange, getDefaultValueForType, getOperatorsForType]);
// Helper to get default value based on field type
const getDefaultValueForType = useCallback((type: string) => {
switch (type) {
case 'boolean':
return true;
case 'number':
return 0;
case 'date':
return '';
default:
return '';
}
}, []);
// Helper to get valid operators for a field type
const getOperatorsForType = useCallback((type: string, isEvent: boolean) => {
if (isEvent) {
return EVENT_OPERATORS;
}
if (type === 'boolean') {
return STANDARD_OPERATORS.filter(op =>
['equals', 'notEquals', 'exists', 'notExists'].includes(op.value)
);
}
if (type === 'number' || type === 'date') {
return STANDARD_OPERATORS.filter(op =>
['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'exists', 'notExists', 'within'].includes(op.value)
);
}
// String type - no within operator, no comparison operators
return STANDARD_OPERATORS.filter(op =>
['equals', 'notEquals', 'contains', 'notContains', 'exists', 'notExists'].includes(op.value)
);
}, []);
// Get label for selected field // Get label for selected field
const getFieldLabel = useCallback(() => { const getFieldLabel = useCallback(() => {
+12 -11
View File
@@ -334,6 +334,18 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [stepToDelete, setStepToDelete] = useState<string | null>(null); const [stepToDelete, setStepToDelete] = useState<string | null>(null);
// Define handlers before they are used in useMemo
const handleEditStep = useCallback((stepId: string) => {
// This will be handled by the parent component
const event = new CustomEvent('workflow-edit-step', {detail: {stepId}});
window.dispatchEvent(event);
}, []);
const handleDeleteStepClick = useCallback((stepId: string) => {
setStepToDelete(stepId);
setShowDeleteDialog(true);
}, []);
// Convert workflow steps to React Flow nodes // Convert workflow steps to React Flow nodes
const rawNodes: Node[] = useMemo(() => { const rawNodes: Node[] = useMemo(() => {
@@ -622,17 +634,6 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
[addStepContext, workflowId, onUpdate], [addStepContext, workflowId, onUpdate],
); );
const handleEditStep = useCallback((stepId: string) => {
// This will be handled by the parent component
const event = new CustomEvent('workflow-edit-step', {detail: {stepId}});
window.dispatchEvent(event);
}, []);
const handleDeleteStepClick = useCallback((stepId: string) => {
setStepToDelete(stepId);
setShowDeleteDialog(true);
}, []);
// Get all steps that will be affected by deleting a step (the step itself + all downstream steps) // Get all steps that will be affected by deleting a step (the step itself + all downstream steps)
const getAffectedSteps = useCallback( const getAffectedSteps = useCallback(
(stepId: string): typeof steps => { (stepId: string): typeof steps => {
-4
View File
@@ -546,10 +546,6 @@ export default function CampaignDetailsPage() {
setHasChanges(true); setHasChanges(true);
}} }}
placeholder="<h1>Welcome!</h1><p>Your email content here...</p>" placeholder="<h1>Welcome!</h1><p>Your email content here...</p>"
canUploadImages={true}
subject={editedCampaign.subject}
from={editedCampaign.from}
replyTo={editedCampaign.replyTo || undefined}
/> />
</CardContent> </CardContent>
</Card> </Card>
-4
View File
@@ -195,10 +195,6 @@ export default function CreateCampaignPage() {
value={body} value={body}
onChange={setBody} onChange={setBody}
placeholder="<h1>Welcome!</h1><p>Your email content here...</p>" placeholder="<h1>Welcome!</h1><p>Your email content here...</p>"
canUploadImages={true}
subject={subject}
from={from}
replyTo={replyTo}
/> />
</div> </div>
</CardContent> </CardContent>
-4
View File
@@ -270,10 +270,6 @@ export default function TemplateEditorPage() {
value={editedTemplate.body || ''} value={editedTemplate.body || ''}
onChange={body => setEditedTemplate({...editedTemplate, body})} onChange={body => setEditedTemplate({...editedTemplate, body})}
placeholder="<h1>Welcome!</h1><p>Thanks for subscribing to our newsletter.</p>" placeholder="<h1>Welcome!</h1><p>Thanks for subscribing to our newsletter.</p>"
canUploadImages={true}
subject={editedTemplate.subject}
from={editedTemplate.from}
replyTo={editedTemplate.replyTo || undefined}
/> />
</CardContent> </CardContent>
</Card> </Card>
-4
View File
@@ -179,10 +179,6 @@ export default function CreateTemplatePage() {
value={body} value={body}
onChange={setBody} onChange={setBody}
placeholder="<h1>Welcome!</h1><p>Thanks for subscribing to our newsletter.</p>" placeholder="<h1>Welcome!</h1><p>Thanks for subscribing to our newsletter.</p>"
canUploadImages={true}
subject={subject}
from={from}
replyTo={replyTo}
/> />
</CardContent> </CardContent>
</Card> </Card>
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+3 -3
View File
@@ -36,14 +36,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-4 sm:p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg max-h-[90vh] overflow-y-auto mx-4 sm:mx-0', 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-4 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[90vh] overflow-y-auto',
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-3 top-3 sm:right-4 sm:top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 p-1 sm:p-0"> <DialogPrimitive.Close className="absolute right-2.5 top-2.5 sm:right-4 sm:top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 p-1 sm:p-1.5">
<X className="h-5 w-5 sm:h-4 sm:w-4" /> <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>
</DialogPrimitive.Content> </DialogPrimitive.Content>