feat: add shared StatusBadge, SourceTag, AgeIndicator components and format utilities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 14:46:29 +05:30
parent 5075fa3f04
commit d36f9f39b5
4 changed files with 162 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import { cx } from '@/utils/cx';
import { daysAgoFromNow, getAgeBracket } from '@/lib/format';
const ageBracketColorMap = {
fresh: 'text-success-primary',
warm: 'text-warning-primary',
cold: 'text-error-primary',
};
interface AgeIndicatorProps {
dateStr: string;
}
export const AgeIndicator = ({ dateStr }: AgeIndicatorProps) => {
const days = daysAgoFromNow(dateStr);
const bracket = getAgeBracket(days);
const colorClass = ageBracketColorMap[bracket];
return (
<span className={cx('text-sm font-semibold', colorClass)}>
{days}d
</span>
);
};

View File

@@ -0,0 +1,45 @@
import { Badge } from '@/components/base/badges/badges';
import type { LeadSource } from '@/types/entities';
type SourceColor = 'blue' | 'pink' | 'success' | 'blue-light' | 'gray' | 'purple' | 'orange' | 'warning';
const sourceColorMap: Record<LeadSource, SourceColor> = {
FACEBOOK_AD: 'blue',
INSTAGRAM: 'pink',
GOOGLE_AD: 'success',
GOOGLE_MY_BUSINESS: 'blue-light',
WHATSAPP: 'success',
WEBSITE: 'gray',
REFERRAL: 'purple',
WALK_IN: 'orange',
PHONE: 'warning',
OTHER: 'gray',
};
const sourceLabelMap: Record<LeadSource, string> = {
FACEBOOK_AD: 'Facebook',
INSTAGRAM: 'Instagram',
GOOGLE_AD: 'Google',
GOOGLE_MY_BUSINESS: 'GMB',
WHATSAPP: 'WhatsApp',
WEBSITE: 'Website',
REFERRAL: 'Referral',
WALK_IN: 'Walk-in',
PHONE: 'Phone',
OTHER: 'Other',
};
interface SourceTagProps {
source: LeadSource;
size?: 'sm' | 'md';
}
export const SourceTag = ({ source, size = 'sm' }: SourceTagProps) => {
const color = sourceColorMap[source];
const label = sourceLabelMap[source];
return (
<Badge size={size} type="pill-color" color={color}>
{label}
</Badge>
);
};

View File

@@ -0,0 +1,54 @@
import { BadgeWithDot } from '@/components/base/badges/badges';
import type { CampaignStatus, LeadStatus } from '@/types/entities';
const toTitleCase = (str: string): string =>
str
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
type LeadStatusColor = 'blue' | 'brand' | 'success' | 'warning' | 'purple' | 'error' | 'gray';
type CampaignStatusColor = 'gray' | 'success' | 'warning' | 'blue';
const leadStatusColorMap: Record<LeadStatus, LeadStatusColor> = {
NEW: 'blue',
CONTACTED: 'brand',
QUALIFIED: 'success',
NURTURING: 'warning',
APPOINTMENT_SET: 'purple',
CONVERTED: 'success',
LOST: 'error',
};
const campaignStatusColorMap: Record<CampaignStatus, CampaignStatusColor> = {
DRAFT: 'gray',
ACTIVE: 'success',
PAUSED: 'warning',
COMPLETED: 'blue',
};
interface LeadStatusBadgeProps {
status: LeadStatus;
}
export const LeadStatusBadge = ({ status }: LeadStatusBadgeProps) => {
const color = leadStatusColorMap[status];
return (
<BadgeWithDot size="sm" type="pill-color" color={color}>
{toTitleCase(status)}
</BadgeWithDot>
);
};
interface CampaignStatusBadgeProps {
status: CampaignStatus;
}
export const CampaignStatusBadge = ({ status }: CampaignStatusBadgeProps) => {
const color = campaignStatusColorMap[status];
return (
<BadgeWithDot size="sm" type="pill-color" color={color}>
{toTitleCase(status)}
</BadgeWithDot>
);
};

39
src/lib/format.ts Normal file
View File

@@ -0,0 +1,39 @@
// Format currency from micros to display string (INR)
export const formatCurrency = (amountMicros: number, currency = 'INR'): string => {
const amount = amountMicros / 1_000_000;
return new Intl.NumberFormat('en-IN', { style: 'currency', currency, maximumFractionDigits: 0 }).format(amount);
};
// Format phone number for display
export const formatPhone = (phone: { number: string; callingCode: string }): string =>
`${phone.callingCode} ${phone.number.replace(/(\d{5})(\d{5})/, '$1 $2')}`;
// Calculate days ago from ISO date string
export const daysAgoFromNow = (dateStr: string): number => {
const diff = Date.now() - new Date(dateStr).getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
};
// Get aging bracket from days
export const getAgeBracket = (days: number): 'fresh' | 'warm' | 'cold' =>
days < 2 ? 'fresh' : days <= 5 ? 'warm' : 'cold';
// Format relative age string
export const formatRelativeAge = (dateStr: string): string => {
const days = daysAgoFromNow(dateStr);
if (days === 0) return 'Today';
if (days === 1) return '1 day ago';
return `${days} days ago`;
};
// Format short date (Mar 15, 2:30 PM)
export const formatShortDate = (dateStr: string): string =>
new Intl.DateTimeFormat('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).format(new Date(dateStr));
// Get initials from a name
export const getInitials = (firstName: string, lastName: string): string =>
`${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase();
// Format large numbers (1234 -> "1.2K", 1234567 -> "1.2M")
export const formatCompact = (n: number): string =>
new Intl.NumberFormat('en-IN', { notation: 'compact', maximumFractionDigits: 1 }).format(n);