Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .cursor/rules/design-context.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
description: Project design context for Conduit-UI. Guides Quintessential rule application to this existing admin panel codebase.
globs: "**/*.{tsx,jsx,ts,js,css}"
alwaysApply: true
---

# Design Context -- Conduit-UI

This is an existing Next.js 16 admin panel. Apply Quintessential standards with awareness of what's already built.

## Current Design Language (preserve)
- Font: Inter via next/font/google -- keep
- Color system: 26 semantic CSS variables in @theme with :root/:dark HSL channels -- extend, don't replace
- Component library: 56 shadcn/ui + Radix primitives in src/components/ui/ -- extend, don't replace
- Class merging: cn() (clsx + twMerge) -- use consistently
- Variants: cva for multi-variant components -- follow this pattern for new components
- Spacing: Tailwind v4 default 4px-based scale -- stay on-scale
- Radius: Tokenized via --radius (0.5rem) with derived md/sm -- use radius tokens, not arbitrary values
- Theme: next-themes with class="dark" toggle -- respect both modes
- Overlays: Radix Dialog/Sheet + Vaul Drawer -- use for new overlays

## Migration Targets (improve incrementally)
- Typography: Add antialiased to body, text-wrap: balance on headings, tabular-nums on data columns
- Colors: Extract hardcoded chart/viz hex values to --color-chart-* tokens
- Colors: Define --popover and --card in :root (aligned with light theme)
- Shadows: Use --shadow-1 through --shadow-4 layered scale; prefer shadow tokens over arbitrary shadow-[...] values
- Animation: prefers-reduced-motion support in globals
- Animation: Standardize on one primary easing curve where practical
- Components: Prefer shared DataTable in src/components/ui/data-table.tsx for TanStack table UIs
- Components: Align Textarea styling with Input (text-sm, ring-2)
- Interaction: Add keyboard shortcut hints to tooltips where relevant
- Interaction: Guard hover-only affordances with @media (hover: hover) where appropriate

## Rules of Engagement
- Apply Quintessential standards to ALL new code
- When modifying existing files, improve what you touch
- Do NOT refactor untouched code unprompted
- Do NOT rewrite working components for style alone
- When you spot a major violation in code you're editing, fix it silently
- When you spot a major violation in code you're NOT editing, leave a brief comment noting the opportunity
- Always use cn() for class merging
- Always use cva for components with more than 2 variant axes
- Always add both light and dark mode styles
- Always use CSS variable colors for theme-bound UI; use chart tokens for charts/viz
- Always respect the spacing scale -- no arbitrary pixel values
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Logs
node_modules
logs
/logs
*.log
npm-debug.log*
.pnpm-debug.log*
Expand Down
83 changes: 83 additions & 0 deletions src/app/(dashboard)/(modules)/communications/logs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getTokens } from '@/lib/api/notifications';
import { CommunicationsLogsTabs } from '@/components/communications/logs/communications-logs-tabs';
import {
PageDescription,
PageHeader,
PageTitle,
} from '@/components/ui/page-header';

type CommunicationsLogsParams = {
searchParams: Promise<{
tab?: string;
messageId?: string;
templateId?: string;
receiver?: string;
sender?: string;
cc?: string;
replyTo?: string;
startDate?: string;
endDate?: string;
skip?: string;
sort?: string;
limit?: string;
search?: string;
platform?: string;
}>;
};

function parseTab(tab: string | undefined): 'email' | 'push' {
return tab === 'push' ? 'push' : 'email';
}

export default async function CommunicationsLogsPage(
props: Readonly<CommunicationsLogsParams>
) {
const searchParams = await props.searchParams;
const tab = parseTab(searchParams.tab);

const pushTokensData =
tab === 'push'
? await getTokens(
Number.parseInt(searchParams.skip ?? '0', 10) || 0,
Number.parseInt(searchParams.limit ?? '20', 10) || 20,
{
sort: searchParams.sort,
search: searchParams.search,
platform: searchParams.platform,
}
)
: undefined;

const refreshPushTokens = async (search: string) => {
'use server';
const { tokens } = await getTokens(
Number.parseInt(searchParams.skip ?? '0', 10) || 0,
Number.parseInt(searchParams.limit ?? '20', 10) || 20,
{
sort: searchParams.sort,
search,
platform: searchParams.platform,
}
);
return tokens;
};

return (
<div className="space-y-6">
<PageHeader>
<div>
<PageTitle>Logs & Devices</PageTitle>
<PageDescription>
Email delivery history and registered push device tokens.
</PageDescription>
</div>
</PageHeader>

<CommunicationsLogsTabs
initialTab={tab}
pushTokensData={pushTokensData}
refreshPushTokens={refreshPushTokens}
/>
</div>
);
}
84 changes: 84 additions & 0 deletions src/app/(dashboard)/(modules)/communications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import { FileText, History, MessagesSquare, Settings } from 'lucide-react';
import { ModuleDashboard } from '@/components/dashboard/ModuleDashboard';
import {
getEmailMetrics,
getModuleStatus,
getModuleUptime,
getPushNotificationMetrics,
getSystemMetrics,
} from '@/lib/prometheus/metrics';
import {
COMMUNICATIONS_SHARED_RUNTIME,
getApiModuleNameFromPath,
} from '@/lib/utils/module-utils';
import { ModuleStatus } from '@/components/dashboard/ModuleStatusCard';
import { QuickAction } from '@/components/dashboard/QuickActionsCard';
import { MetricCardProps } from '@/components/dashboard/MetricCard';
import { getPrometheusAvailability } from '@/lib/observability/prometheusAvailability';

export default async function CommunicationsDashboard() {
const promAvailability = await getPrometheusAvailability();
const [emailMetrics, pushMetrics] = await Promise.all([
getEmailMetrics(),
getPushNotificationMetrics(),
]);
const metrics = [...emailMetrics, ...pushMetrics];

const metricCards: MetricCardProps[] = metrics.map(metric => ({
title: metric.name,
value: metric.value,
description: metric.description,
status: metric.status,
}));

const apiModuleName =
getApiModuleNameFromPath('/communications') || 'communications';

const systemMetrics = await getSystemMetrics(apiModuleName);
const uptime = await getModuleUptime(apiModuleName);
const status = await getModuleStatus(apiModuleName);

const moduleStatus: ModuleStatus = {
name: 'Communications',
status: status,
uptime: uptime,
version: '1.0.0',
instances: 1,
description: 'Email, SMS, and push notifications',
};

const quickActions: QuickAction[] = [
{
title: 'Templates',
description: 'Manage email, SMS, and push templates',
icon: <FileText className="h-4 w-4" />,
href: '/communications/templates',
},
{
title: 'View logs',
description: 'Email logs and push device tokens',
icon: <History className="h-4 w-4" />,
href: '/communications/logs',
},
{
title: 'Channel settings',
description: 'Configure email, SMS, and push providers',
icon: <Settings className="h-4 w-4" />,
href: '/communications/settings',
},
];

return (
<ModuleDashboard
moduleName="Communications"
moduleIcon={<MessagesSquare className="h-8 w-8" />}
moduleStatus={moduleStatus}
metrics={metricCards}
systemMetrics={systemMetrics}
quickActions={quickActions}
prometheusState={promAvailability.state}
sharedRuntime={COMMUNICATIONS_SHARED_RUNTIME}
/>
);
}
57 changes: 57 additions & 0 deletions src/app/(dashboard)/(modules)/communications/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getEmailSettings } from '@/lib/api/email';
import { getSmsSettings } from '@/lib/api/sms';
import { getNotificationSettings } from '@/lib/api/notifications';
import { CommunicationsSettingsTabs } from '@/components/communications/settings/communications-settings-tabs';
import {
PageDescription,
PageHeader,
PageTitle,
} from '@/components/ui/page-header';

type CommunicationsSettingsParams = {
searchParams: Promise<{
tab?: string;
}>;
};

function parseTab(tab: string | undefined): 'email' | 'sms' | 'push' {
if (tab === 'sms' || tab === 'push') return tab;
return 'email';
}

export default async function CommunicationsSettingsPage(
props: Readonly<CommunicationsSettingsParams>
) {
const searchParams = await props.searchParams;
const tab = parseTab(searchParams.tab);

const [
{ config: emailSettings },
{ config: smsSettings },
{ config: pushSettings },
] = await Promise.all([
getEmailSettings(),
getSmsSettings(),
getNotificationSettings(),
]);

return (
<div className="space-y-6">
<PageHeader>
<div>
<PageTitle>Settings</PageTitle>
<PageDescription>
Configure email, SMS, and push providers.
</PageDescription>
</div>
</PageHeader>

<CommunicationsSettingsTabs
initialTab={tab}
emailSettings={emailSettings}
smsSettings={smsSettings}
pushSettings={pushSettings}
/>
</div>
);
}
Loading
Loading