feat: implement CommandPalette for enhanced navigation and recent pages tracking

This commit is contained in:
Dries Augustyns
2026-04-24 18:42:04 +02:00
parent f40cc86cec
commit 1c89ed083e
12 changed files with 582 additions and 45 deletions
+28
View File
@@ -8,9 +8,11 @@ import {DefaultSeo} from 'next-seo';
import {NuqsAdapter} from 'nuqs/adapters/next/pages';
import {Loader} from '@plunk/ui';
import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
import {CommandPalette} from '../components/CommandPalette';
import {useProjects} from '../lib/hooks/useProject';
import {useUser} from '../lib/hooks/useUser';
import {network} from '../lib/network';
import {addRecentPage, getFallbackLabel} from '../lib/recentPages';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import advancedFormat from 'dayjs/plugin/advancedFormat';
@@ -135,6 +137,31 @@ function Root(props: AppProps) {
route => router.pathname === route || router.pathname.startsWith(`${route}/`),
);
useEffect(() => {
const handleRouteChange = () => {
const pathname = window.location.pathname;
// Pass 1: record immediately with a route-pattern label so detail pages
// are always captured even before their data loads.
const fallback = getFallbackLabel(pathname);
if (fallback) {
addRecentPage({label: fallback, href: pathname});
}
// Pass 2: after data loads, replace the label with the page's own title
// (e.g. contact email set by NextSeo after the API response arrives).
setTimeout(() => {
const titleLabel = document.title.split(' | ')[0]?.trim();
if (titleLabel && titleLabel !== 'Plunk' && titleLabel !== fallback) {
addRecentPage({label: titleLabel, href: pathname});
}
}, 800);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => router.events.off('routeChangeComplete', handleRouteChange);
}, [router.events]);
return (
<>
<Toaster position={'top-right'} />
@@ -145,6 +172,7 @@ function Root(props: AppProps) {
<App {...props} />
) : (
<ProjectGuard>
<CommandPalette />
<App {...props} />
</ProjectGuard>
)}
+3
View File
@@ -59,6 +59,7 @@ import {useRouter} from 'next/router';
import {useEffect, useState} from 'react';
import {toast} from 'sonner';
import useSWR from 'swr';
import {NextSeo} from 'next-seo';
import {useActiveProject} from '../../lib/contexts/ActiveProjectProvider';
interface CampaignStats {
@@ -338,6 +339,7 @@ export default function CampaignDetailsPage() {
if (isEditMode) {
return (
<DashboardLayout>
<NextSeo title={campaign.data.name} />
<form onSubmit={handleSave} className={`space-y-6 ${hasChanges ? 'pb-32' : ''}`}>
{/* Header */}
<div className="space-y-4">
@@ -842,6 +844,7 @@ export default function CampaignDetailsPage() {
// Render stats view for sent/scheduled campaigns
return (
<DashboardLayout>
<NextSeo title={campaign.data.name} />
<div className="space-y-6">
{/* Header */}
<div className="space-y-4">
+2
View File
@@ -22,6 +22,7 @@ import {useRouter} from 'next/router';
import {useEffect, useState} from 'react';
import {toast} from 'sonner';
import useSWR from 'swr';
import {NextSeo} from 'next-seo';
import type {FilterCondition} from '@plunk/types';
import {SegmentSchemas} from '@plunk/shared';
import {SegmentFilterBuilder} from '../../components/SegmentFilterBuilder';
@@ -210,6 +211,7 @@ export default function SegmentDetailPage() {
return (
<DashboardLayout>
<NextSeo title={segment.name} />
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
+2
View File
@@ -19,6 +19,7 @@ import {network} from '../../lib/network';
import {useChangeTracking} from '../../lib/hooks/useChangeTracking';
import {ArrowLeft, Save, Trash2, TriangleAlert} from 'lucide-react';
import Link from 'next/link';
import {NextSeo} from 'next-seo';
import {useRouter} from 'next/router';
import {useEffect, useState} from 'react';
import {toast} from 'sonner';
@@ -126,6 +127,7 @@ export default function TemplateEditorPage() {
return (
<DashboardLayout>
<NextSeo title={template.name} />
<form onSubmit={handleSave} className={`max-w-5xl mx-auto space-y-6 ${hasChanges ? 'pb-32' : ''}`}>
{/* Header */}
<div className="space-y-4">
+2
View File
@@ -62,6 +62,7 @@ import {useRouter} from 'next/router';
import {useEffect, useState} from 'react';
import {toast} from 'sonner';
import useSWR from 'swr';
import {NextSeo} from 'next-seo';
import {WorkflowBuilder} from '../../components/WorkflowBuilder';
import {KeyValueEditor} from '../../components/KeyValueEditor';
import {TemplateSearchPicker} from '../../components/TemplateSearchPicker';
@@ -440,6 +441,7 @@ export default function WorkflowEditorPage() {
return (
<DashboardLayout>
<NextSeo title={workflow.name} />
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3 sm:gap-4">