diff --git a/src/components/shared/age-indicator.tsx b/src/components/shared/age-indicator.tsx new file mode 100644 index 0000000..53024bd --- /dev/null +++ b/src/components/shared/age-indicator.tsx @@ -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 ( + + {days}d + + ); +}; diff --git a/src/components/shared/source-tag.tsx b/src/components/shared/source-tag.tsx new file mode 100644 index 0000000..c006d0c --- /dev/null +++ b/src/components/shared/source-tag.tsx @@ -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 = { + 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 = { + 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 ( + + {label} + + ); +}; diff --git a/src/components/shared/status-badge.tsx b/src/components/shared/status-badge.tsx new file mode 100644 index 0000000..03172cb --- /dev/null +++ b/src/components/shared/status-badge.tsx @@ -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 = { + NEW: 'blue', + CONTACTED: 'brand', + QUALIFIED: 'success', + NURTURING: 'warning', + APPOINTMENT_SET: 'purple', + CONVERTED: 'success', + LOST: 'error', +}; + +const campaignStatusColorMap: Record = { + DRAFT: 'gray', + ACTIVE: 'success', + PAUSED: 'warning', + COMPLETED: 'blue', +}; + +interface LeadStatusBadgeProps { + status: LeadStatus; +} + +export const LeadStatusBadge = ({ status }: LeadStatusBadgeProps) => { + const color = leadStatusColorMap[status]; + return ( + + {toTitleCase(status)} + + ); +}; + +interface CampaignStatusBadgeProps { + status: CampaignStatus; +} + +export const CampaignStatusBadge = ({ status }: CampaignStatusBadgeProps) => { + const color = campaignStatusColorMap[status]; + return ( + + {toTitleCase(status)} + + ); +}; diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..d9a5e6f --- /dev/null +++ b/src/lib/format.ts @@ -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);