mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 08:16:31 -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",
|
||||
};
|
||||
Reference in New Issue
Block a user