Enhance forms with validation and terminology

Implements enhanced inline validation with contextual messages and examples, adds a comprehensive theme park terminology tool (tooltip and glossary), and integrates these features into ParkForm and RideForm (including header actions and descriptive hints). Also introduces new helper modules and components to support validated inputs and glossary tooltips.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 23:25:15 +00:00
parent 67ce8b5a88
commit 985454f0d9
8 changed files with 875 additions and 37 deletions

View File

@@ -0,0 +1,171 @@
/**
* Enhanced Validation Messages
* Provides contextual, helpful error messages with examples
*/
export const validationMessages = {
slug: {
format: 'Slug must contain only lowercase letters, numbers, and hyphens. Example: "steel-vengeance" or "millennium-force"',
required: 'Slug is required. It will be used in the URL. Example: "fury-325"',
duplicate: 'This slug is already in use. Try adding a location or number: "thunder-run-kentucky"',
},
url: {
format: 'Must be a valid URL starting with http:// or https://. Example: "https://www.cedarpoint.com"',
protocol: 'URL must start with http:// or https://. Add the protocol to your URL.',
},
email: {
format: 'Must be a valid email address. Example: "contact@park.com"',
},
phone: {
format: 'Enter phone number in any format. Examples: "+1-419-555-0123" or "(419) 555-0123"',
maxLength: (max: number) => `Phone number must be less than ${max} characters`,
},
dates: {
future: 'Opening date cannot be in the future. Use today or an earlier date.',
closingBeforeOpening: 'Closing date must be after opening date. Check both dates for accuracy.',
invalidFormat: 'Invalid date format. Use the date picker or enter in YYYY-MM-DD format.',
precision: 'Select how precise this date is (exact, month, year, etc.)',
},
numbers: {
heightRequirement: 'Height must be in centimeters, between 0-300. Example: "122" for 122cm (48 inches)',
speed: 'Speed must be in km/h, between 0-500. Example: "193" for 193 km/h (120 mph)',
length: 'Length must be in meters. Example: "1981" for 1,981 meters (6,500 feet)',
height: 'Height must be in meters. Example: "94" for 94 meters (310 feet)',
gForce: 'G-force must be between -10 and 10. Example: "4.5" for 4.5 positive Gs',
inversions: 'Number of inversions (upside-down elements). Example: "7"',
capacity: 'Capacity per hour must be between 1-99,999. Example: "1200" for 1,200 riders/hour',
duration: 'Duration in seconds. Example: "180" for 3 minutes',
positive: 'Value must be a positive number',
range: (min: number, max: number) => `Value must be between ${min} and ${max}`,
},
text: {
required: 'This field is required',
maxLength: (max: number, current?: number) =>
current ? `${current}/${max} characters. Please shorten by ${current - max} characters.` : `Maximum ${max} characters`,
minLength: (min: number) => `Must be at least ${min} characters`,
noHtml: 'HTML tags are not allowed. Use plain text only.',
trimmed: 'Extra spaces at the beginning or end will be removed',
},
park: {
nameRequired: 'Park name is required. Example: "Cedar Point" or "Six Flags Magic Mountain"',
typeRequired: 'Select a park type (theme park, amusement park, water park, etc.)',
statusRequired: 'Select the current status (operating, closed, under construction, etc.)',
locationRequired: 'Location is required. Use the search to find or add a location.',
operatorHelp: 'The company that operates the park (e.g., Cedar Fair, Six Flags)',
ownerHelp: 'The company that owns the property (often same as operator)',
},
ride: {
nameRequired: 'Ride name is required. Example: "Steel Vengeance" or "Maverick"',
categoryRequired: 'Select a ride category (roller coaster, flat ride, water ride, etc.)',
parkRequired: 'Park is required. Select or create the park where this ride is located.',
manufacturerHelp: 'Company that manufactured the ride (e.g., RMC, Intamin, B&M)',
designerHelp: 'Company that designed the ride (if different from manufacturer)',
trackMaterial: 'Materials used for the track. Common: Steel, Wood, Hybrid (RMC IBox)',
supportMaterial: 'Materials used for support structure. Common: Steel, Wood',
propulsionMethod: 'How the ride is propelled. Common: LSM Launch, Chain Lift, Hydraulic Launch',
},
company: {
nameRequired: 'Company name is required. Example: "Rocky Mountain Construction"',
typeRequired: 'Select company type (manufacturer, designer, operator, property owner)',
countryHelp: 'Country where the company is headquartered',
},
units: {
metricOnly: 'All measurements must be in metric units (m, km, cm, kg, km/h, etc.)',
metricExamples: 'Use metric: m (meters), km/h (speed), cm (centimeters), kg (weight)',
imperialNote: 'The system will automatically convert to imperial for users who prefer it',
temperature: 'Temperature must be in Celsius. Example: "25" for 25°C (77°F)',
},
submission: {
sourceUrl: 'Where did you find this information? Helps moderators verify accuracy. Example: manufacturer website, news article, park map',
notes: 'Add context for moderators. Example: "Confirmed via park press release" or "Specifications approximate"',
notesMaxLength: 'Submission notes must be less than 1000 characters',
},
};
/**
* Common validation helpers
*/
export const validationHelpers = {
/**
* Check if a URL has proper protocol
*/
hasProtocol: (url: string): boolean => {
return url.startsWith('http://') || url.startsWith('https://');
},
/**
* Suggest adding protocol to URL
*/
suggestProtocol: (url: string): string => {
if (!url) return '';
if (validationHelpers.hasProtocol(url)) return url;
return `https://${url}`;
},
/**
* Format a slug from a name
*/
formatSlug: (name: string): string => {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
},
/**
* Check if date is in the future
*/
isFutureDate: (date: string | Date): boolean => {
const d = new Date(date);
return d > new Date();
},
/**
* Format character count display
*/
formatCharCount: (current: number, max: number): string => {
const remaining = max - current;
if (remaining < 0) {
return `${current}/${max} (${Math.abs(remaining)} over limit)`;
}
if (remaining < 50) {
return `${current}/${max} (${remaining} remaining)`;
}
return `${current}/${max}`;
},
};
/**
* Field-specific validation hints for FormDescription
*/
export const fieldHints = {
slug: 'URL-friendly identifier using lowercase letters, numbers, and hyphens only',
websiteUrl: 'Official website URL (must start with https:// or http://)',
email: 'Contact email for the park or ride operator',
phone: 'Contact phone number in any format',
heightRequirement: 'Minimum height in centimeters (metric). Will be converted for display.',
ageRequirement: 'Minimum age requirement in years',
capacity: 'Theoretical maximum riders per hour under optimal conditions',
duration: 'Typical ride duration in seconds from dispatch to return',
speed: 'Maximum speed in km/h (metric). Will be converted for display.',
height: 'Maximum height in meters (metric). Will be converted for display.',
length: 'Track/route length in meters (metric). Will be converted for display.',
inversions: 'Total number of elements where riders go upside down (≥90 degrees)',
gForce: 'Maximum positive or negative G-forces experienced',
sourceUrl: 'Reference link to verify this information (Wikipedia, official site, news article, etc.)',
submissionNotes: 'Help moderators understand your submission (how you verified the info, any uncertainties, etc.)',
};