mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-14 01:54:43 -05:00
feat: polish invoice editor and viewer UI with custom NumberInput
component - Create custom NumberInput component with increment/decrement buttons - Add 0.25 step increments for hours and rates in invoice forms - Implement emerald-themed styling with hover states and accessibility - Add keyboard navigation (arrow keys) and proper ARIA support - Condense invoice editor tax/totals section into efficient grid layout - Update client dropdown to single-line format (name + email) - Add fixed footer with floating action bar pattern matching business forms - Redesign invoice viewer with better space utilization and visual hierarchy - Maintain professional appearance and consistent design system - Fix Next.js 15 params Promise handling across all invoice pages - Resolve TypeScript compilation errors and type-only imports
This commit is contained in:
388
src/lib/form-constants.ts
Normal file
388
src/lib/form-constants.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* Shared form constants and utilities
|
||||
*/
|
||||
|
||||
// US States
|
||||
export const US_STATES = [
|
||||
{ value: "AL", label: "Alabama" },
|
||||
{ value: "AK", label: "Alaska" },
|
||||
{ value: "AZ", label: "Arizona" },
|
||||
{ value: "AR", label: "Arkansas" },
|
||||
{ value: "CA", label: "California" },
|
||||
{ value: "CO", label: "Colorado" },
|
||||
{ value: "CT", label: "Connecticut" },
|
||||
{ value: "DE", label: "Delaware" },
|
||||
{ value: "FL", label: "Florida" },
|
||||
{ value: "GA", label: "Georgia" },
|
||||
{ value: "HI", label: "Hawaii" },
|
||||
{ value: "ID", label: "Idaho" },
|
||||
{ value: "IL", label: "Illinois" },
|
||||
{ value: "IN", label: "Indiana" },
|
||||
{ value: "IA", label: "Iowa" },
|
||||
{ value: "KS", label: "Kansas" },
|
||||
{ value: "KY", label: "Kentucky" },
|
||||
{ value: "LA", label: "Louisiana" },
|
||||
{ value: "ME", label: "Maine" },
|
||||
{ value: "MD", label: "Maryland" },
|
||||
{ value: "MA", label: "Massachusetts" },
|
||||
{ value: "MI", label: "Michigan" },
|
||||
{ value: "MN", label: "Minnesota" },
|
||||
{ value: "MS", label: "Mississippi" },
|
||||
{ value: "MO", label: "Missouri" },
|
||||
{ value: "MT", label: "Montana" },
|
||||
{ value: "NE", label: "Nebraska" },
|
||||
{ value: "NV", label: "Nevada" },
|
||||
{ value: "NH", label: "New Hampshire" },
|
||||
{ value: "NJ", label: "New Jersey" },
|
||||
{ value: "NM", label: "New Mexico" },
|
||||
{ value: "NY", label: "New York" },
|
||||
{ value: "NC", label: "North Carolina" },
|
||||
{ value: "ND", label: "North Dakota" },
|
||||
{ value: "OH", label: "Ohio" },
|
||||
{ value: "OK", label: "Oklahoma" },
|
||||
{ value: "OR", label: "Oregon" },
|
||||
{ value: "PA", label: "Pennsylvania" },
|
||||
{ value: "RI", label: "Rhode Island" },
|
||||
{ value: "SC", label: "South Carolina" },
|
||||
{ value: "SD", label: "South Dakota" },
|
||||
{ value: "TN", label: "Tennessee" },
|
||||
{ value: "TX", label: "Texas" },
|
||||
{ value: "UT", label: "Utah" },
|
||||
{ value: "VT", label: "Vermont" },
|
||||
{ value: "VA", label: "Virginia" },
|
||||
{ value: "WA", label: "Washington" },
|
||||
{ value: "WV", label: "West Virginia" },
|
||||
{ value: "WI", label: "Wisconsin" },
|
||||
{ value: "WY", label: "Wyoming" },
|
||||
];
|
||||
|
||||
// Most commonly used countries
|
||||
export const POPULAR_COUNTRIES = [
|
||||
{ value: "United States", label: "United States" },
|
||||
{ value: "United Kingdom", label: "United Kingdom" },
|
||||
{ value: "Canada", label: "Canada" },
|
||||
{ value: "Australia", label: "Australia" },
|
||||
{ value: "Germany", label: "Germany" },
|
||||
{ value: "France", label: "France" },
|
||||
{ value: "India", label: "India" },
|
||||
{ value: "Japan", label: "Japan" },
|
||||
{ value: "Mexico", label: "Mexico" },
|
||||
{ value: "Brazil", label: "Brazil" },
|
||||
];
|
||||
|
||||
// All countries with ISO codes
|
||||
export const ALL_COUNTRIES = [
|
||||
{ value: "Afghanistan", label: "Afghanistan" },
|
||||
{ value: "Albania", label: "Albania" },
|
||||
{ value: "Algeria", label: "Algeria" },
|
||||
{ value: "Andorra", label: "Andorra" },
|
||||
{ value: "Angola", label: "Angola" },
|
||||
{ value: "Antigua and Barbuda", label: "Antigua and Barbuda" },
|
||||
{ value: "Argentina", label: "Argentina" },
|
||||
{ value: "Armenia", label: "Armenia" },
|
||||
{ value: "Australia", label: "Australia" },
|
||||
{ value: "Austria", label: "Austria" },
|
||||
{ value: "Azerbaijan", label: "Azerbaijan" },
|
||||
{ value: "Bahamas", label: "Bahamas" },
|
||||
{ value: "Bahrain", label: "Bahrain" },
|
||||
{ value: "Bangladesh", label: "Bangladesh" },
|
||||
{ value: "Barbados", label: "Barbados" },
|
||||
{ value: "Belarus", label: "Belarus" },
|
||||
{ value: "Belgium", label: "Belgium" },
|
||||
{ value: "Belize", label: "Belize" },
|
||||
{ value: "Benin", label: "Benin" },
|
||||
{ value: "Bhutan", label: "Bhutan" },
|
||||
{ value: "Bolivia", label: "Bolivia" },
|
||||
{ value: "Bosnia and Herzegovina", label: "Bosnia and Herzegovina" },
|
||||
{ value: "Botswana", label: "Botswana" },
|
||||
{ value: "Brazil", label: "Brazil" },
|
||||
{ value: "Brunei", label: "Brunei" },
|
||||
{ value: "Bulgaria", label: "Bulgaria" },
|
||||
{ value: "Burkina Faso", label: "Burkina Faso" },
|
||||
{ value: "Burundi", label: "Burundi" },
|
||||
{ value: "Cabo Verde", label: "Cabo Verde" },
|
||||
{ value: "Cambodia", label: "Cambodia" },
|
||||
{ value: "Cameroon", label: "Cameroon" },
|
||||
{ value: "Canada", label: "Canada" },
|
||||
{ value: "Central African Republic", label: "Central African Republic" },
|
||||
{ value: "Chad", label: "Chad" },
|
||||
{ value: "Chile", label: "Chile" },
|
||||
{ value: "China", label: "China" },
|
||||
{ value: "Colombia", label: "Colombia" },
|
||||
{ value: "Comoros", label: "Comoros" },
|
||||
{ value: "Congo", label: "Congo" },
|
||||
{ value: "Costa Rica", label: "Costa Rica" },
|
||||
{ value: "Croatia", label: "Croatia" },
|
||||
{ value: "Cuba", label: "Cuba" },
|
||||
{ value: "Cyprus", label: "Cyprus" },
|
||||
{ value: "Czech Republic", label: "Czech Republic" },
|
||||
{
|
||||
value: "Democratic Republic of the Congo",
|
||||
label: "Democratic Republic of the Congo",
|
||||
},
|
||||
{ value: "Denmark", label: "Denmark" },
|
||||
{ value: "Djibouti", label: "Djibouti" },
|
||||
{ value: "Dominica", label: "Dominica" },
|
||||
{ value: "Dominican Republic", label: "Dominican Republic" },
|
||||
{ value: "East Timor", label: "East Timor" },
|
||||
{ value: "Ecuador", label: "Ecuador" },
|
||||
{ value: "Egypt", label: "Egypt" },
|
||||
{ value: "El Salvador", label: "El Salvador" },
|
||||
{ value: "Equatorial Guinea", label: "Equatorial Guinea" },
|
||||
{ value: "Eritrea", label: "Eritrea" },
|
||||
{ value: "Estonia", label: "Estonia" },
|
||||
{ value: "Eswatini", label: "Eswatini" },
|
||||
{ value: "Ethiopia", label: "Ethiopia" },
|
||||
{ value: "Fiji", label: "Fiji" },
|
||||
{ value: "Finland", label: "Finland" },
|
||||
{ value: "France", label: "France" },
|
||||
{ value: "Gabon", label: "Gabon" },
|
||||
{ value: "Gambia", label: "Gambia" },
|
||||
{ value: "Georgia", label: "Georgia" },
|
||||
{ value: "Germany", label: "Germany" },
|
||||
{ value: "Ghana", label: "Ghana" },
|
||||
{ value: "Greece", label: "Greece" },
|
||||
{ value: "Grenada", label: "Grenada" },
|
||||
{ value: "Guatemala", label: "Guatemala" },
|
||||
{ value: "Guinea", label: "Guinea" },
|
||||
{ value: "Guinea-Bissau", label: "Guinea-Bissau" },
|
||||
{ value: "Guyana", label: "Guyana" },
|
||||
{ value: "Haiti", label: "Haiti" },
|
||||
{ value: "Honduras", label: "Honduras" },
|
||||
{ value: "Hungary", label: "Hungary" },
|
||||
{ value: "Iceland", label: "Iceland" },
|
||||
{ value: "India", label: "India" },
|
||||
{ value: "Indonesia", label: "Indonesia" },
|
||||
{ value: "Iran", label: "Iran" },
|
||||
{ value: "Iraq", label: "Iraq" },
|
||||
{ value: "Ireland", label: "Ireland" },
|
||||
{ value: "Israel", label: "Israel" },
|
||||
{ value: "Italy", label: "Italy" },
|
||||
{ value: "Ivory Coast", label: "Ivory Coast" },
|
||||
{ value: "Jamaica", label: "Jamaica" },
|
||||
{ value: "Japan", label: "Japan" },
|
||||
{ value: "Jordan", label: "Jordan" },
|
||||
{ value: "Kazakhstan", label: "Kazakhstan" },
|
||||
{ value: "Kenya", label: "Kenya" },
|
||||
{ value: "Kiribati", label: "Kiribati" },
|
||||
{ value: "Kuwait", label: "Kuwait" },
|
||||
{ value: "Kyrgyzstan", label: "Kyrgyzstan" },
|
||||
{ value: "Laos", label: "Laos" },
|
||||
{ value: "Latvia", label: "Latvia" },
|
||||
{ value: "Lebanon", label: "Lebanon" },
|
||||
{ value: "Lesotho", label: "Lesotho" },
|
||||
{ value: "Liberia", label: "Liberia" },
|
||||
{ value: "Libya", label: "Libya" },
|
||||
{ value: "Liechtenstein", label: "Liechtenstein" },
|
||||
{ value: "Lithuania", label: "Lithuania" },
|
||||
{ value: "Luxembourg", label: "Luxembourg" },
|
||||
{ value: "Madagascar", label: "Madagascar" },
|
||||
{ value: "Malawi", label: "Malawi" },
|
||||
{ value: "Malaysia", label: "Malaysia" },
|
||||
{ value: "Maldives", label: "Maldives" },
|
||||
{ value: "Mali", label: "Mali" },
|
||||
{ value: "Malta", label: "Malta" },
|
||||
{ value: "Marshall Islands", label: "Marshall Islands" },
|
||||
{ value: "Mauritania", label: "Mauritania" },
|
||||
{ value: "Mauritius", label: "Mauritius" },
|
||||
{ value: "Mexico", label: "Mexico" },
|
||||
{ value: "Micronesia", label: "Micronesia" },
|
||||
{ value: "Moldova", label: "Moldova" },
|
||||
{ value: "Monaco", label: "Monaco" },
|
||||
{ value: "Mongolia", label: "Mongolia" },
|
||||
{ value: "Montenegro", label: "Montenegro" },
|
||||
{ value: "Morocco", label: "Morocco" },
|
||||
{ value: "Mozambique", label: "Mozambique" },
|
||||
{ value: "Myanmar", label: "Myanmar" },
|
||||
{ value: "Namibia", label: "Namibia" },
|
||||
{ value: "Nauru", label: "Nauru" },
|
||||
{ value: "Nepal", label: "Nepal" },
|
||||
{ value: "Netherlands", label: "Netherlands" },
|
||||
{ value: "New Zealand", label: "New Zealand" },
|
||||
{ value: "Nicaragua", label: "Nicaragua" },
|
||||
{ value: "Niger", label: "Niger" },
|
||||
{ value: "Nigeria", label: "Nigeria" },
|
||||
{ value: "North Korea", label: "North Korea" },
|
||||
{ value: "North Macedonia", label: "North Macedonia" },
|
||||
{ value: "Norway", label: "Norway" },
|
||||
{ value: "Oman", label: "Oman" },
|
||||
{ value: "Pakistan", label: "Pakistan" },
|
||||
{ value: "Palau", label: "Palau" },
|
||||
{ value: "Palestine", label: "Palestine" },
|
||||
{ value: "Panama", label: "Panama" },
|
||||
{ value: "Papua New Guinea", label: "Papua New Guinea" },
|
||||
{ value: "Paraguay", label: "Paraguay" },
|
||||
{ value: "Peru", label: "Peru" },
|
||||
{ value: "Philippines", label: "Philippines" },
|
||||
{ value: "Poland", label: "Poland" },
|
||||
{ value: "Portugal", label: "Portugal" },
|
||||
{ value: "Qatar", label: "Qatar" },
|
||||
{ value: "Romania", label: "Romania" },
|
||||
{ value: "Russia", label: "Russia" },
|
||||
{ value: "Rwanda", label: "Rwanda" },
|
||||
{ value: "Saint Kitts and Nevis", label: "Saint Kitts and Nevis" },
|
||||
{ value: "Saint Lucia", label: "Saint Lucia" },
|
||||
{
|
||||
value: "Saint Vincent and the Grenadines",
|
||||
label: "Saint Vincent and the Grenadines",
|
||||
},
|
||||
{ value: "Samoa", label: "Samoa" },
|
||||
{ value: "San Marino", label: "San Marino" },
|
||||
{ value: "Sao Tome and Principe", label: "Sao Tome and Principe" },
|
||||
{ value: "Saudi Arabia", label: "Saudi Arabia" },
|
||||
{ value: "Senegal", label: "Senegal" },
|
||||
{ value: "Serbia", label: "Serbia" },
|
||||
{ value: "Seychelles", label: "Seychelles" },
|
||||
{ value: "Sierra Leone", label: "Sierra Leone" },
|
||||
{ value: "Singapore", label: "Singapore" },
|
||||
{ value: "Slovakia", label: "Slovakia" },
|
||||
{ value: "Slovenia", label: "Slovenia" },
|
||||
{ value: "Solomon Islands", label: "Solomon Islands" },
|
||||
{ value: "Somalia", label: "Somalia" },
|
||||
{ value: "South Africa", label: "South Africa" },
|
||||
{ value: "South Korea", label: "South Korea" },
|
||||
{ value: "South Sudan", label: "South Sudan" },
|
||||
{ value: "Spain", label: "Spain" },
|
||||
{ value: "Sri Lanka", label: "Sri Lanka" },
|
||||
{ value: "Sudan", label: "Sudan" },
|
||||
{ value: "Suriname", label: "Suriname" },
|
||||
{ value: "Sweden", label: "Sweden" },
|
||||
{ value: "Switzerland", label: "Switzerland" },
|
||||
{ value: "Syria", label: "Syria" },
|
||||
{ value: "Taiwan", label: "Taiwan" },
|
||||
{ value: "Tajikistan", label: "Tajikistan" },
|
||||
{ value: "Tanzania", label: "Tanzania" },
|
||||
{ value: "Thailand", label: "Thailand" },
|
||||
{ value: "Togo", label: "Togo" },
|
||||
{ value: "Tonga", label: "Tonga" },
|
||||
{ value: "Trinidad and Tobago", label: "Trinidad and Tobago" },
|
||||
{ value: "Tunisia", label: "Tunisia" },
|
||||
{ value: "Turkey", label: "Turkey" },
|
||||
{ value: "Turkmenistan", label: "Turkmenistan" },
|
||||
{ value: "Tuvalu", label: "Tuvalu" },
|
||||
{ value: "Uganda", label: "Uganda" },
|
||||
{ value: "Ukraine", label: "Ukraine" },
|
||||
{ value: "United Arab Emirates", label: "United Arab Emirates" },
|
||||
{ value: "United Kingdom", label: "United Kingdom" },
|
||||
{ value: "United States", label: "United States" },
|
||||
{ value: "Uruguay", label: "Uruguay" },
|
||||
{ value: "Uzbekistan", label: "Uzbekistan" },
|
||||
{ value: "Vanuatu", label: "Vanuatu" },
|
||||
{ value: "Vatican City", label: "Vatican City" },
|
||||
{ value: "Venezuela", label: "Venezuela" },
|
||||
{ value: "Vietnam", label: "Vietnam" },
|
||||
{ value: "Yemen", label: "Yemen" },
|
||||
{ value: "Zambia", label: "Zambia" },
|
||||
{ value: "Zimbabwe", label: "Zimbabwe" },
|
||||
];
|
||||
|
||||
// Phone number formatting
|
||||
export function formatPhoneNumber(value: string): string {
|
||||
// Remove all non-numeric characters
|
||||
const phoneNumber = value.replace(/\D/g, "");
|
||||
|
||||
// Format as US phone number
|
||||
if (phoneNumber.length <= 3) {
|
||||
return phoneNumber;
|
||||
} else if (phoneNumber.length <= 6) {
|
||||
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`;
|
||||
} else if (phoneNumber.length <= 10) {
|
||||
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 10)}`;
|
||||
} else {
|
||||
// Handle international numbers
|
||||
return `+${phoneNumber.slice(0, phoneNumber.length - 10)} (${phoneNumber.slice(-10, -7)}) ${phoneNumber.slice(-7, -4)}-${phoneNumber.slice(-4)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Email validation
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// URL formatting
|
||||
export function formatWebsiteUrl(url: string): string {
|
||||
if (!url) return "";
|
||||
|
||||
// If URL doesn't start with http:// or https://, add https://
|
||||
if (!url.match(/^https?:\/\//i)) {
|
||||
return `https://${url}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// Postal code formatting
|
||||
export function formatPostalCode(
|
||||
value: string,
|
||||
country: string = "United States",
|
||||
): string {
|
||||
if (country === "United States") {
|
||||
// Format as US ZIP code (12345 or 12345-6789)
|
||||
const digits = value.replace(/\D/g, "");
|
||||
if (digits.length <= 5) {
|
||||
return digits;
|
||||
} else {
|
||||
return `${digits.slice(0, 5)}-${digits.slice(5, 9)}`;
|
||||
}
|
||||
} else if (country === "Canada") {
|
||||
// Format as Canadian postal code (A1A 1A1)
|
||||
const cleaned = value.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
if (cleaned.length <= 3) {
|
||||
return cleaned;
|
||||
} else {
|
||||
return `${cleaned.slice(0, 3)} ${cleaned.slice(3, 6)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Return as-is for other countries
|
||||
return value;
|
||||
}
|
||||
|
||||
// Tax ID formatting
|
||||
export function formatTaxId(value: string, type: string = "EIN"): string {
|
||||
const digits = value.replace(/\D/g, "");
|
||||
|
||||
if (type === "EIN") {
|
||||
// Format as XX-XXXXXXX
|
||||
if (digits.length <= 2) {
|
||||
return digits;
|
||||
} else {
|
||||
return `${digits.slice(0, 2)}-${digits.slice(2, 9)}`;
|
||||
}
|
||||
} else if (type === "SSN") {
|
||||
// Format as XXX-XX-XXXX
|
||||
if (digits.length <= 3) {
|
||||
return digits;
|
||||
} else if (digits.length <= 5) {
|
||||
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
||||
} else {
|
||||
return `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Form validation messages
|
||||
export const VALIDATION_MESSAGES = {
|
||||
required: "This field is required",
|
||||
email: "Please enter a valid email address",
|
||||
phone: "Please enter a valid phone number",
|
||||
url: "Please enter a valid URL",
|
||||
postalCode: "Please enter a valid postal code",
|
||||
taxId: "Please enter a valid tax ID",
|
||||
};
|
||||
|
||||
// Form field placeholders
|
||||
export const PLACEHOLDERS = {
|
||||
name: "Enter name",
|
||||
email: "email@example.com",
|
||||
phone: "(555) 123-4567",
|
||||
addressLine1: "123 Main Street",
|
||||
addressLine2: "Suite 100",
|
||||
city: "San Francisco",
|
||||
postalCode: "12345",
|
||||
website: "www.example.com",
|
||||
taxId: "12-3456789",
|
||||
};
|
||||
36
src/lib/navigation.ts
Normal file
36
src/lib/navigation.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
Settings,
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
FileText,
|
||||
Building,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface NavLink {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
export interface NavSection {
|
||||
title: string;
|
||||
links: NavLink[];
|
||||
}
|
||||
|
||||
export const navigationConfig: NavSection[] = [
|
||||
{
|
||||
title: "Main",
|
||||
links: [
|
||||
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
|
||||
{ name: "Clients", href: "/dashboard/clients", icon: Users },
|
||||
{ name: "Businesses", href: "/dashboard/businesses", icon: Building },
|
||||
{ name: "Invoices", href: "/dashboard/invoices", icon: FileText },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Account",
|
||||
links: [
|
||||
{ name: "Settings", href: "/dashboard/settings", icon: Settings },
|
||||
],
|
||||
},
|
||||
];
|
||||
121
src/lib/pluralize.ts
Normal file
121
src/lib/pluralize.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Pluralization rules for common entities in the app
|
||||
*/
|
||||
const PLURALIZATION_RULES: Record<string, { singular: string; plural: string }> = {
|
||||
business: { singular: "Business", plural: "Businesses" },
|
||||
client: { singular: "Client", plural: "Clients" },
|
||||
invoice: { singular: "Invoice", plural: "Invoices" },
|
||||
setting: { singular: "Setting", plural: "Settings" },
|
||||
user: { singular: "User", plural: "Users" },
|
||||
payment: { singular: "Payment", plural: "Payments" },
|
||||
item: { singular: "Item", plural: "Items" },
|
||||
tax: { singular: "Tax", plural: "Taxes" },
|
||||
category: { singular: "Category", plural: "Categories" },
|
||||
company: { singular: "Company", plural: "Companies" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the plural form of a word
|
||||
*/
|
||||
export function pluralize(word: string, count?: number): string {
|
||||
// If count is provided and is 1, return singular
|
||||
if (count === 1) {
|
||||
return word;
|
||||
}
|
||||
|
||||
const lowerWord = word.toLowerCase();
|
||||
|
||||
// Check if we have a specific rule for this word
|
||||
if (PLURALIZATION_RULES[lowerWord]) {
|
||||
return PLURALIZATION_RULES[lowerWord].plural;
|
||||
}
|
||||
|
||||
// Apply general pluralization rules
|
||||
// Words ending in s, ss, sh, ch, x, z
|
||||
if (/(?:s|ss|sh|ch|x|z)$/i.test(word)) {
|
||||
return word + "es";
|
||||
}
|
||||
|
||||
// Words ending in consonant + y
|
||||
if (/[^aeiou]y$/i.test(word)) {
|
||||
return word.slice(0, -1) + "ies";
|
||||
}
|
||||
|
||||
// Words ending in f or fe
|
||||
if (/(?:f|fe)$/i.test(word)) {
|
||||
return word.replace(/(?:f|fe)$/i, "ves");
|
||||
}
|
||||
|
||||
// Default: just add 's'
|
||||
return word + "s";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singular form of a word
|
||||
*/
|
||||
export function singularize(word: string): string {
|
||||
const lowerWord = word.toLowerCase();
|
||||
|
||||
// Check if we have a specific rule for this word (search by plural)
|
||||
const rule = Object.values(PLURALIZATION_RULES).find(
|
||||
(r) => r.plural.toLowerCase() === lowerWord
|
||||
);
|
||||
|
||||
if (rule) {
|
||||
return rule.singular;
|
||||
}
|
||||
|
||||
// Apply general singularization rules
|
||||
// Words ending in ies
|
||||
if (/ies$/i.test(word)) {
|
||||
return word.slice(0, -3) + "y";
|
||||
}
|
||||
|
||||
// Words ending in es
|
||||
if (/(?:s|ss|sh|ch|x|z)es$/i.test(word)) {
|
||||
return word.slice(0, -2);
|
||||
}
|
||||
|
||||
// Words ending in ves
|
||||
if (/ves$/i.test(word)) {
|
||||
return word.slice(0, -3) + "f";
|
||||
}
|
||||
|
||||
// Words ending in s
|
||||
if (/s$/i.test(word) && word.length > 1) {
|
||||
return word.slice(0, -1);
|
||||
}
|
||||
|
||||
// Default: return as is
|
||||
return word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a word
|
||||
*/
|
||||
export function capitalize(word: string): string {
|
||||
if (!word) return word;
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a properly formatted label for a route segment
|
||||
*/
|
||||
export function getRouteLabel(segment: string, isPlural: boolean = true): string {
|
||||
// First, check if it's already in our rules
|
||||
const rule = PLURALIZATION_RULES[segment.toLowerCase()];
|
||||
if (rule) {
|
||||
return isPlural ? rule.plural : rule.singular;
|
||||
}
|
||||
|
||||
// If not, try to find it by plural form
|
||||
const singularForm = singularize(segment);
|
||||
const singularRule = PLURALIZATION_RULES[singularForm.toLowerCase()];
|
||||
if (singularRule) {
|
||||
return isPlural ? singularRule.plural : singularRule.singular;
|
||||
}
|
||||
|
||||
// Otherwise, just capitalize and optionally pluralize
|
||||
const capitalized = capitalize(segment);
|
||||
return isPlural ? pluralize(capitalized) : capitalized;
|
||||
}
|
||||
Reference in New Issue
Block a user