feat: Improved createdAt and updatedAt visualisation

This commit is contained in:
Dries Augustyns
2025-12-11 10:57:56 +01:00
parent 6b25bbe2f8
commit 1019ba0d82
15 changed files with 320 additions and 245 deletions
+1
View File
@@ -37,6 +37,7 @@
"@uiw/react-codemirror": "^4.25.3", "@uiw/react-codemirror": "^4.25.3",
"@xyflow/react": "^12.9.3", "@xyflow/react": "^12.9.3",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.19",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"juice": "^11.0.3", "juice": "^11.0.3",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
+8 -14
View File
@@ -1,4 +1,4 @@
import {memo, useCallback, useEffect, useMemo, useState} from 'react'; import {memo, useEffect, useMemo, useState} from 'react';
import {useForm} from 'react-hook-form'; import {useForm} from 'react-hook-form';
import {zodResolver} from '@hookform/resolvers/zod'; import {zodResolver} from '@hookform/resolvers/zod';
import {BillingLimitSchemas} from '@plunk/shared'; import {BillingLimitSchemas} from '@plunk/shared';
@@ -23,7 +23,7 @@ import {
import {AlertCircle, AlertTriangle, Check} from 'lucide-react'; import {AlertCircle, AlertTriangle, Check} from 'lucide-react';
import {AnimatePresence, motion} from 'framer-motion'; import {AnimatePresence, motion} from 'framer-motion';
import type {z} from 'zod'; import type {z} from 'zod';
import {useBillingLimits, type BillingLimitsData, type CategoryLimit} from '../lib/hooks/useBillingLimits'; import {type BillingLimitsData, type CategoryLimit, useBillingLimits} from '../lib/hooks/useBillingLimits';
import {network} from '../lib/network'; import {network} from '../lib/network';
// Price per email in the smallest currency unit (e.g., cents for USD/EUR) // Price per email in the smallest currency unit (e.g., cents for USD/EUR)
@@ -50,7 +50,7 @@ const formatEmailCost = (emailCount: number, currency: string | null): string =>
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
}).format(costInCents / 100); }).format(costInCents / 100);
} catch (error) { } catch {
// Fallback if currency is invalid // Fallback if currency is invalid
return `${(costInCents / 100).toFixed(2)} ${currency.toUpperCase()}`; return `${(costInCents / 100).toFixed(2)} ${currency.toUpperCase()}`;
} }
@@ -167,8 +167,8 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<div className="ml-2"> <div className="ml-2">
<p className="text-sm"> <p className="text-sm">
You&apos;re on the free tier with 1,000 emails per month. Upgrade to a paid subscription for You&apos;re on the free tier with 1,000 emails per month. Upgrade to a paid subscription for unlimited
unlimited emails or custom limits. emails or custom limits.
</p> </p>
</div> </div>
</Alert> </Alert>
@@ -254,9 +254,7 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Maximum workflow emails per month. Leave empty for unlimited. Maximum workflow emails per month. Leave empty for unlimited.
{estimatedCost && ( {estimatedCost && <span className="text-neutral-500"> {estimatedCost}/month</span>}
<span className="text-neutral-500"> {estimatedCost}/month</span>
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -286,9 +284,7 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Maximum campaign emails per month. Leave empty for unlimited. Maximum campaign emails per month. Leave empty for unlimited.
{estimatedCost && ( {estimatedCost && <span className="text-neutral-500"> {estimatedCost}/month</span>}
<span className="text-neutral-500"> {estimatedCost}/month</span>
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -318,9 +314,7 @@ export function BillingLimits({projectId, hasSubscription, billingEnabled}: Bill
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Maximum transactional emails per month. Leave empty for unlimited. Maximum transactional emails per month. Leave empty for unlimited.
{estimatedCost && ( {estimatedCost && <span className="text-neutral-500"> {estimatedCost}/month</span>}
<span className="text-neutral-500"> {estimatedCost}/month</span>
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -13,10 +13,9 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
Input, Input,
Label, Label
} from '@plunk/ui'; } from '@plunk/ui';
import type {Template} from '@plunk/db'; import type {Template} from '@plunk/db';
import {network} from '../lib/network';
import {ArrowLeft, FileText, Search} from 'lucide-react'; import {ArrowLeft, FileText, Search} from 'lucide-react';
import {useState} from 'react'; import {useState} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -411,11 +410,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
<Button variant="outline" onClick={handleBack} className="flex-1"> <Button variant="outline" onClick={handleBack} className="flex-1">
Back Back
</Button> </Button>
<Button <Button onClick={handleConfirm} className="flex-1" disabled={!Object.values(selectedFields).some(v => v)}>
onClick={handleConfirm}
className="flex-1"
disabled={!Object.values(selectedFields).some(v => v)}
>
Create Campaign Create Campaign
</Button> </Button>
</div> </div>
+7
View File
@@ -10,6 +10,13 @@ import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
import {useProjects} from '../lib/hooks/useProject'; import {useProjects} from '../lib/hooks/useProject';
import {useUser} from '../lib/hooks/useUser'; import {useUser} from '../lib/hooks/useUser';
import {network} from '../lib/network'; import {network} from '../lib/network';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import advancedFormat from 'dayjs/plugin/advancedFormat';
// Configure dayjs plugins globally
dayjs.extend(relativeTime);
dayjs.extend(advancedFormat);
// Routes that don't require authentication // Routes that don't require authentication
const PUBLIC_ROUTES = ['/auth/login', '/auth/signup', '/auth/reset']; const PUBLIC_ROUTES = ['/auth/login', '/auth/signup', '/auth/reset'];
+9 -12
View File
@@ -397,7 +397,9 @@ export default function CampaignDetailsPage() {
<TestTube className="h-4 w-4 mt-0.5 text-neutral-700" /> <TestTube className="h-4 w-4 mt-0.5 text-neutral-700" />
<div className="flex flex-col gap-0.5 flex-1"> <div className="flex flex-col gap-0.5 flex-1">
<span className="font-medium text-sm">Send Test Email</span> <span className="font-medium text-sm">Send Test Email</span>
<span className="text-xs text-neutral-500 leading-snug">Preview in your inbox before sending</span> <span className="text-xs text-neutral-500 leading-snug">
Preview in your inbox before sending
</span>
</div> </div>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
@@ -406,7 +408,9 @@ export default function CampaignDetailsPage() {
<Send className="h-4 w-4 mt-0.5 text-neutral-700" /> <Send className="h-4 w-4 mt-0.5 text-neutral-700" />
<div className="flex flex-col gap-0.5 flex-1"> <div className="flex flex-col gap-0.5 flex-1">
<span className="font-medium text-sm">Send Now</span> <span className="font-medium text-sm">Send Now</span>
<span className="text-xs text-neutral-500 leading-snug">Send immediately to all recipients</span> <span className="text-xs text-neutral-500 leading-snug">
Send immediately to all recipients
</span>
</div> </div>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
@@ -576,7 +580,8 @@ export default function CampaignDetailsPage() {
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" /> <Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
<p className="text-xs text-blue-800"> <p className="text-xs text-blue-800">
This count will be recalculated right before sending to ensure accuracy. The final number may differ if contacts subscribe, unsubscribe, or segment membership changes. This count will be recalculated right before sending to ensure accuracy. The final number may
differ if contacts subscribe, unsubscribe, or segment membership changes.
</p> </p>
</div> </div>
</div> </div>
@@ -1004,9 +1009,7 @@ export default function CampaignDetailsPage() {
{c.status === CampaignStatus.SCHEDULED && ( {c.status === CampaignStatus.SCHEDULED && (
<div className="flex items-start gap-1.5 p-2 bg-blue-50 border border-blue-200 rounded"> <div className="flex items-start gap-1.5 p-2 bg-blue-50 border border-blue-200 rounded">
<Info className="h-3 w-3 text-blue-600 mt-0.5 flex-shrink-0" /> <Info className="h-3 w-3 text-blue-600 mt-0.5 flex-shrink-0" />
<p className="text-xs text-blue-800"> <p className="text-xs text-blue-800">Recipient count will be recalculated at send time</p>
Recipient count will be recalculated at send time
</p>
</div> </div>
)} )}
</div> </div>
@@ -1026,12 +1029,6 @@ export default function CampaignDetailsPage() {
</div> </div>
</div> </div>
)} )}
{/* Created */}
<div>
<p className="text-xs font-medium text-neutral-500 uppercase tracking-wide mb-2">Created</p>
<p className="text-sm text-neutral-900">{new Date(c.createdAt).toLocaleDateString()}</p>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+2 -2
View File
@@ -78,7 +78,7 @@ export default function CreateCampaignPage() {
setBody(template.data.body); setBody(template.data.body);
toast.success('Template loaded successfully'); toast.success('Template loaded successfully');
} catch (error) { } catch {
toast.error('Failed to load template'); toast.error('Failed to load template');
} finally { } finally {
setLoadingTemplate(false); setLoadingTemplate(false);
@@ -104,7 +104,7 @@ export default function CreateCampaignPage() {
setBody(campaign.data.body); setBody(campaign.data.body);
toast.success('Campaign loaded successfully'); toast.success('Campaign loaded successfully');
} catch (error) { } catch {
toast.error('Failed to load campaign'); toast.error('Failed to load campaign');
} finally { } finally {
setLoadingTemplate(false); setLoadingTemplate(false);
+20
View File
@@ -30,6 +30,7 @@ import {useRouter} from 'next/router';
import {useState} from 'react'; import {useState} from 'react';
import {toast} from 'sonner'; import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import dayjs from 'dayjs';
interface PaginatedCampaigns { interface PaginatedCampaigns {
campaigns: Campaign[]; campaigns: Campaign[];
@@ -465,6 +466,25 @@ export default function CampaignsPage() {
)} )}
</div> </div>
{/* Metadata */}
<div className="flex items-center gap-4 text-xs text-neutral-500 pt-3 border-t border-neutral-100">
<div className="flex items-center gap-1.5">
<Calendar className="h-3 w-3" />
<div className="group relative inline-block cursor-help">
<span>Created {dayjs(campaign.createdAt).fromNow()}</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">
{dayjs(campaign.createdAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
<div className="group relative inline-block cursor-help">
<span> Updated {dayjs(campaign.updatedAt).fromNow()}</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">
{dayjs(campaign.updatedAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
{/* Actions */} {/* Actions */}
<div className="flex gap-2 pt-2 border-t border-neutral-100"> <div className="flex gap-2 pt-2 border-t border-neutral-100">
<Link href={`/campaigns/${campaign.id}`} className="flex-1"> <Link href={`/campaigns/${campaign.id}`} className="flex-1">
+23 -30
View File
@@ -7,20 +7,21 @@ import {
CardTitle, CardTitle,
ConfirmDialog, ConfirmDialog,
Input, Input,
Label, Label
} from '@plunk/ui'; } from '@plunk/ui';
import type {Contact} from '@plunk/db'; import type {Contact} from '@plunk/db';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {KeyValueEditor} from '../../components/KeyValueEditor'; import {KeyValueEditor} from '../../components/KeyValueEditor';
import {ActivityFeed} from '../../components/ActivityFeed'; import {ActivityFeed} from '../../components/ActivityFeed';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {ArrowLeft, Calendar, Copy, Database, ExternalLink, Mail, Save, Settings, Trash2} from 'lucide-react'; import {ArrowLeft, Copy, Database, ExternalLink, Mail, Save, Settings, Trash2} from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {toast} from 'sonner'; import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import {ContactSchemas} from '@plunk/shared'; import {ContactSchemas} from '@plunk/shared';
import dayjs from 'dayjs';
export default function ContactDetailPage() { export default function ContactDetailPage() {
const router = useRouter(); const router = useRouter();
@@ -250,34 +251,6 @@ export default function ContactDetailPage() {
</div> </div>
</div> </div>
<div className="flex items-start gap-3">
<Calendar className="h-5 w-5 text-neutral-500 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-neutral-900">Created</p>
<p className="text-sm text-neutral-500">
{new Date(contact.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Calendar className="h-5 w-5 text-neutral-500 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
<p className="text-sm text-neutral-500">
{new Date(contact.updatedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
</div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Database className="h-5 w-5 text-neutral-500 mt-0.5" /> <Database className="h-5 w-5 text-neutral-500 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -285,6 +258,26 @@ export default function ContactDetailPage() {
<p className="text-xs text-neutral-500 font-mono break-all">{contact.id}</p> <p className="text-xs text-neutral-500 font-mono break-all">{contact.id}</p>
</div> </div>
</div> </div>
<div>
<p className="text-sm font-medium text-neutral-900">Created</p>
<div className="group relative inline-block cursor-help">
<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">
{dayjs(contact.createdAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
<div>
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
<div className="group relative inline-block cursor-help">
<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">
{dayjs(contact.updatedAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
+15 -6
View File
@@ -13,7 +13,7 @@ import {
DialogTitle, DialogTitle,
Input, Input,
Label, Label,
Switch, Switch
} from '@plunk/ui'; } from '@plunk/ui';
import type {Contact} from '@plunk/db'; import type {Contact} from '@plunk/db';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
@@ -32,7 +32,7 @@ import {
Search, Search,
Trash2, Trash2,
Upload, Upload,
XCircle, XCircle
} from 'lucide-react'; } from 'lucide-react';
import {NextSeo} from 'next-seo'; import {NextSeo} from 'next-seo';
import Link from 'next/link'; import Link from 'next/link';
@@ -40,6 +40,7 @@ import {useEffect, useRef, useState} from 'react';
import {toast} from 'sonner'; import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import {ContactSchemas} from '@plunk/shared'; import {ContactSchemas} from '@plunk/shared';
import dayjs from 'dayjs';
interface PaginatedContacts { interface PaginatedContacts {
contacts: Contact[]; contacts: Contact[];
@@ -279,7 +280,12 @@ export default function ContactsPage() {
</span> </span>
</td> </td>
<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">
{new Date(contact.createdAt).toLocaleDateString()} <div className="group relative inline-block cursor-help">
{dayjs(contact.createdAt).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">
{dayjs(contact.createdAt).format('DD MMMM YYYY, hh:mm')}
</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">
@@ -324,9 +330,12 @@ export default function ContactsPage() {
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-neutral-500"> <div className="group relative inline-block cursor-help">
{new Date(contact.createdAt).toLocaleDateString()} <span className="text-xs text-neutral-500">{dayjs(contact.createdAt).fromNow()}</span>
</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">
{dayjs(contact.createdAt).format('Do MMMM YYYY, h:mm A')}
</div>
</div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Link href={`/contacts/${contact.id}`}> <Link href={`/contacts/${contact.id}`}>
<Button variant="ghost" size="sm"> <Button variant="ghost" size="sm">
+22 -17
View File
@@ -12,7 +12,7 @@ import {
import type {Contact, Segment} from '@plunk/db'; import type {Contact, Segment} from '@plunk/db';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {ArrowLeft, Calendar, Database, Filter, MailCheck, MailX, RefreshCw, Save, Trash2, Users} from 'lucide-react'; import {ArrowLeft, Database, Filter, MailCheck, MailX, RefreshCw, Save, Trash2, Users} from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
@@ -21,6 +21,7 @@ import useSWR from 'swr';
import type {FilterCondition} from '@plunk/types'; import type {FilterCondition} from '@plunk/types';
import {SegmentSchemas} from '@plunk/shared'; import {SegmentSchemas} from '@plunk/shared';
import {SegmentFilterBuilder} from '../../components/SegmentFilterBuilder'; import {SegmentFilterBuilder} from '../../components/SegmentFilterBuilder';
import dayjs from 'dayjs';
interface PaginatedContacts { interface PaginatedContacts {
contacts: Contact[]; contacts: Contact[];
@@ -388,22 +389,6 @@ export default function SegmentDetailPage() {
<CardTitle>Metadata</CardTitle> <CardTitle>Metadata</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-start gap-3">
<Calendar className="h-5 w-5 text-neutral-500 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-neutral-900">Created</p>
<p className="text-sm text-neutral-500">{new Date(segment.createdAt).toLocaleDateString()}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Calendar className="h-5 w-5 text-neutral-500 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
<p className="text-sm text-neutral-500">{new Date(segment.updatedAt).toLocaleDateString()}</p>
</div>
</div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Database className="h-5 w-5 text-neutral-500 mt-0.5" /> <Database className="h-5 w-5 text-neutral-500 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -411,6 +396,26 @@ export default function SegmentDetailPage() {
<p className="text-xs text-neutral-500 font-mono break-all">{segment.id}</p> <p className="text-xs text-neutral-500 font-mono break-all">{segment.id}</p>
</div> </div>
</div> </div>
<div>
<p className="text-sm font-medium text-neutral-900">Created</p>
<div className="group relative inline-block cursor-help">
<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">
{dayjs(segment.createdAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
<div>
<p className="text-sm font-medium text-neutral-900">Last Updated</p>
<div className="group relative inline-block cursor-help">
<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">
{dayjs(segment.updatedAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+18 -3
View File
@@ -13,12 +13,13 @@ import type {Segment} from '@plunk/db';
import type {FilterCondition} from '@plunk/types'; import type {FilterCondition} from '@plunk/types';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {AlertTriangle, Edit, Filter, Plus, Trash2, Users} from 'lucide-react'; import {AlertTriangle, Calendar, Edit, Filter, Plus, Trash2, Users} from 'lucide-react';
import {NextSeo} from 'next-seo'; import {NextSeo} from 'next-seo';
import Link from 'next/link'; import Link from 'next/link';
import {useState} from 'react'; import {useState} from 'react';
import {toast} from 'sonner'; import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import dayjs from 'dayjs';
// Helper function to count total filters in a condition // Helper function to count total filters in a condition
function countFiltersInCondition(condition: unknown): number { function countFiltersInCondition(condition: unknown): number {
@@ -194,8 +195,22 @@ export default function SegmentsPage() {
</div> </div>
{/* Metadata */} {/* Metadata */}
<div className="text-xs text-neutral-500 pt-2 border-t border-neutral-200"> <div className="flex items-center gap-4 text-xs text-neutral-500 pt-3 border-t border-neutral-100">
Created {new Date(segment.createdAt).toLocaleDateString()} <div className="flex items-center gap-1.5">
<Calendar className="h-3 w-3" />
<div className="group relative inline-block cursor-help">
<span>Created {dayjs(segment.createdAt).fromNow()}</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">
{dayjs(segment.createdAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
<div className="group relative inline-block cursor-help">
<span> Updated {dayjs(segment.updatedAt).fromNow()}</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">
{dayjs(segment.updatedAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
+18 -3
View File
@@ -12,12 +12,13 @@ import {
import type {Template} from '@plunk/db'; import type {Template} from '@plunk/db';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {Copy, Edit, FileText, Plus, Search, Trash2} from 'lucide-react'; import {Calendar, Copy, Edit, FileText, Plus, Search, Trash2} from 'lucide-react';
import {NextSeo} from 'next-seo'; import {NextSeo} from 'next-seo';
import Link from 'next/link'; import Link from 'next/link';
import {useState} from 'react'; import {useState} from 'react';
import {toast} from 'sonner'; import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import dayjs from 'dayjs';
interface PaginatedTemplates { interface PaginatedTemplates {
templates: Template[]; templates: Template[];
@@ -252,8 +253,22 @@ export default function TemplatesPage() {
<p className="text-xs text-neutral-500 mb-1">From</p> <p className="text-xs text-neutral-500 mb-1">From</p>
<p className="text-sm text-neutral-700">{template.from}</p> <p className="text-sm text-neutral-700">{template.from}</p>
</div> </div>
<div className="flex items-center gap-6 text-sm text-neutral-500 pt-2 border-t border-neutral-100"> <div className="flex items-center gap-4 text-xs text-neutral-500 pt-3 border-t border-neutral-100">
<div>Created {new Date(template.createdAt).toLocaleDateString()}</div> <div className="flex items-center gap-1.5">
<Calendar className="h-3 w-3" />
<div className="group relative inline-block cursor-help">
<span>Created {dayjs(template.createdAt).fromNow()}</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">
{dayjs(template.createdAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
<div className="group relative inline-block cursor-help">
<span> Updated {dayjs(template.updatedAt).fromNow()}</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">
{dayjs(template.updatedAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
{template.replyTo && ( {template.replyTo && (
<div> <div>
Reply to: <span className="text-neutral-700">{template.replyTo}</span> Reply to: <span className="text-neutral-700">{template.replyTo}</span>
+7 -1
View File
@@ -57,6 +57,7 @@ import useSWR from 'swr';
import {WorkflowBuilder} from '../../components/WorkflowBuilder'; import {WorkflowBuilder} from '../../components/WorkflowBuilder';
import {ReactFlowProvider} from '@xyflow/react'; import {ReactFlowProvider} from '@xyflow/react';
import {WorkflowSchemas} from '@plunk/shared'; import {WorkflowSchemas} from '@plunk/shared';
import dayjs from 'dayjs';
interface WorkflowWithDetails extends Workflow { interface WorkflowWithDetails extends Workflow {
steps: (WorkflowStep & { steps: (WorkflowStep & {
@@ -611,7 +612,12 @@ export default function WorkflowEditorPage() {
{execution.currentStep?.name ?? '-'} {execution.currentStep?.name ?? '-'}
</td> </td>
<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">
{new Date(execution.startedAt).toLocaleString()} <div className="group relative inline-block cursor-help">
{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">
{dayjs(execution.startedAt).format('DD MMMM YYYY, hh:mm')}
</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">
{(execution.status === 'RUNNING' || execution.status === 'WAITING') && ( {(execution.status === 'RUNNING' || execution.status === 'WAITING') && (
+19 -2
View File
@@ -23,13 +23,14 @@ import {
import type {Workflow} from '@plunk/db'; import type {Workflow} from '@plunk/db';
import {DashboardLayout} from '../../components/DashboardLayout'; import {DashboardLayout} from '../../components/DashboardLayout';
import {network} from '../../lib/network'; import {network} from '../../lib/network';
import {Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon} from 'lucide-react'; import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon} from 'lucide-react';
import {NextSeo} from 'next-seo'; import {NextSeo} from 'next-seo';
import Link from 'next/link'; import Link from 'next/link';
import {useState} from 'react'; import {useState} from 'react';
import {toast} from 'sonner'; import {toast} from 'sonner';
import useSWR from 'swr'; import useSWR from 'swr';
import {WorkflowSchemas} from '@plunk/shared'; import {WorkflowSchemas} from '@plunk/shared';
import dayjs from 'dayjs';
interface PaginatedWorkflows { interface PaginatedWorkflows {
workflows: (Workflow & {_count?: {steps: number; executions: number}})[]; workflows: (Workflow & {_count?: {steps: number; executions: number}})[];
@@ -252,7 +253,23 @@ export default function WorkflowsPage() {
<span className="font-medium text-neutral-900">{workflow._count?.executions ?? 0}</span>{' '} <span className="font-medium text-neutral-900">{workflow._count?.executions ?? 0}</span>{' '}
executions executions
</div> </div>
<div>Created {new Date(workflow.createdAt).toLocaleDateString()}</div> </div>
<div className="flex items-center gap-4 text-xs text-neutral-500 pt-3 border-t border-neutral-100">
<div className="flex items-center gap-1.5">
<Calendar className="h-3 w-3" />
<div className="group relative inline-block cursor-help">
<span>Created {dayjs(workflow.createdAt).fromNow()}</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">
{dayjs(workflow.createdAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div>
<div className="group relative inline-block cursor-help">
<span> Updated {dayjs(workflow.updatedAt).fromNow()}</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">
{dayjs(workflow.updatedAt).format('DD MMMM YYYY, hh:mm')}
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
+1
View File
@@ -19026,6 +19026,7 @@ __metadata:
"@uiw/react-codemirror": "npm:^4.25.3" "@uiw/react-codemirror": "npm:^4.25.3"
"@xyflow/react": "npm:^12.9.3" "@xyflow/react": "npm:^12.9.3"
dagre: "npm:^0.8.5" dagre: "npm:^0.8.5"
dayjs: "npm:^1.11.19"
framer-motion: "npm:^12.23.24" framer-motion: "npm:^12.23.24"
juice: "npm:^11.0.3" juice: "npm:^11.0.3"
lucide-react: "npm:^0.553.0" lucide-react: "npm:^0.553.0"