diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 45b1c76b..d8f83f8e 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -31,6 +31,9 @@ import { OperatorForm } from './OperatorForm'; import { PropertyOwnerForm } from './PropertyOwnerForm'; import { Checkbox } from '@/components/ui/checkbox'; import { SubmissionHelpDialog } from '@/components/help/SubmissionHelpDialog'; +import { TerminologyDialog } from '@/components/help/TerminologyDialog'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { fieldHints } from '@/lib/enhancedValidation'; const parkSchema = z.object({ name: z.string().min(1, 'Park name is required'), @@ -320,10 +323,14 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: {isEditing ? 'Edit Park' : 'Create New Park'} - +
+ + +
+
{/* Basic Information */}
@@ -724,6 +731,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: )}
+
{/* Operator Modal */} diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index f430c3c1..8de50a0c 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -36,6 +36,9 @@ import { TechnicalSpecsEditor, validateTechnicalSpecs } from './editors/Technica import { CoasterStatsEditor, validateCoasterStats } from './editors/CoasterStatsEditor'; import { FormerNamesEditor } from './editors/FormerNamesEditor'; import { SubmissionHelpDialog } from '@/components/help/SubmissionHelpDialog'; +import { TerminologyDialog } from '@/components/help/TerminologyDialog'; +import { TermTooltip } from '@/components/ui/term-tooltip'; +import { fieldHints } from '@/lib/enhancedValidation'; import { convertValueToMetric, convertValueFromMetric, @@ -391,7 +394,10 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: {isEditing ? 'Edit Ride' : 'Create New Ride'} - +
+ + +
@@ -851,17 +857,15 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
- - - - - - -

Material used for the track. Select multiple if hybrid (e.g., wood track with steel supports).

-
-
+
-

Select all materials used in the track

+

+ Common: Steel, Wood, Hybrid (RMC IBox) +

{TRACK_MATERIALS.map((material) => (
@@ -888,16 +892,10 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
- - - - - -

Material used for the support structure. Can be different from track material (e.g., wood track on steel supports).

-
-
-

Select all materials used in the supports

+

+ Materials used for support structure (can differ from track) +

{SUPPORT_MATERIALS.map((material) => (
@@ -923,23 +921,15 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
- - - - - - -

Common methods:

-
    -
  • LSM Launch: Linear Synchronous Motor (smooth, modern)
  • -
  • Chain Lift: Traditional lift hill
  • -
  • Hydraulic Launch: Fast, powerful (e.g., Kingda Ka)
  • -
  • Gravity: Free-fall or terrain-based
  • -
-
-
+
-

Select all propulsion methods used

+

+ Common: LSM Launch, Chain Lift, Hydraulic Launch +

{PROPULSION_METHODS.map((method) => (
diff --git a/src/components/admin/editors/TechnicalSpecsEditor.tsx b/src/components/admin/editors/TechnicalSpecsEditor.tsx index e6050af7..8efd4fc4 100644 --- a/src/components/admin/editors/TechnicalSpecsEditor.tsx +++ b/src/components/admin/editors/TechnicalSpecsEditor.tsx @@ -8,6 +8,7 @@ import { Card } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useUnitPreferences } from "@/hooks/useUnitPreferences"; import { toast } from "sonner"; +import { fieldHints } from "@/lib/enhancedValidation"; import { convertValueToMetric, convertValueFromMetric, diff --git a/src/components/help/TerminologyDialog.tsx b/src/components/help/TerminologyDialog.tsx new file mode 100644 index 00000000..5478acd3 --- /dev/null +++ b/src/components/help/TerminologyDialog.tsx @@ -0,0 +1,135 @@ +import { useState } from "react"; +import { BookOpen, Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { getAllCategories, getTermsByCategory, searchGlossary, type GlossaryTerm } from "@/lib/glossary"; + +export function TerminologyDialog() { + const [open, setOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + + const categories = getAllCategories(); + const searchResults = searchQuery ? searchGlossary(searchQuery) : []; + + const renderTermCard = (term: GlossaryTerm) => ( +
+
+

{term.term}

+ + {term.category.replace('-', ' ')} + +
+

{term.definition}

+ {term.example && ( +

+ Example: {term.example} +

+ )} + {term.relatedTerms && term.relatedTerms.length > 0 && ( +
+ Related: + {term.relatedTerms.map(rt => ( + + {rt.replace(/-/g, ' ')} + + ))} +
+ )} +
+ ); + + return ( + + + + + + + Theme Park Terminology Reference + + Quick reference for technical terms, manufacturers, and ride types + + + +
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ + {/* Results */} + {searchQuery ? ( + +
+ {searchResults.length > 0 ? ( + searchResults.map(renderTermCard) + ) : ( +
+ No terms found matching "{searchQuery}" +
+ )} +
+
+ ) : ( + + + {categories.map(cat => ( + + {cat === 'manufacturer' ? 'Mfg.' : + cat === 'technology' ? 'Tech' : + cat === 'measurement' ? 'Units' : + cat.charAt(0).toUpperCase() + cat.slice(1).substring(0, 4)} + + ))} + + + {categories.map(cat => { + const terms = getTermsByCategory(cat); + return ( + + +
+
+

+ {cat.replace('-', ' ')} +

+ {terms.length} terms +
+ {terms.map(renderTermCard)} +
+
+
+ ); + })} +
+ )} +
+ +
+ Tip + Hover over underlined terms in forms to see quick definitions +
+
+
+ ); +} diff --git a/src/components/ui/term-tooltip.tsx b/src/components/ui/term-tooltip.tsx new file mode 100644 index 00000000..03f8a37c --- /dev/null +++ b/src/components/ui/term-tooltip.tsx @@ -0,0 +1,56 @@ +import { HelpCircle } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { getGlossaryTerm } from "@/lib/glossary"; +import { cn } from "@/lib/utils"; + +interface TermTooltipProps { + term: string; + children: React.ReactNode; + inline?: boolean; + showIcon?: boolean; +} + +export function TermTooltip({ term, children, inline = false, showIcon = true }: TermTooltipProps) { + const glossaryEntry = getGlossaryTerm(term); + + if (!glossaryEntry) { + return <>{children}; + } + + return ( + + + + {children} + {showIcon && ( + + )} + + + +
+
{glossaryEntry.term}
+

+ {glossaryEntry.category.replace('-', ' ')} +

+

{glossaryEntry.definition}

+ {glossaryEntry.example && ( +

+ Example: {glossaryEntry.example} +

+ )} + {glossaryEntry.relatedTerms && glossaryEntry.relatedTerms.length > 0 && ( +

+ See also: {glossaryEntry.relatedTerms.map(t => + getGlossaryTerm(t)?.term || t + ).join(', ')} +

+ )} +
+
+
+ ); +} diff --git a/src/components/ui/validated-input.tsx b/src/components/ui/validated-input.tsx new file mode 100644 index 00000000..f92c5b94 --- /dev/null +++ b/src/components/ui/validated-input.tsx @@ -0,0 +1,87 @@ +import * as React from "react"; +import { Check, AlertCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { useDebouncedCallback } from "use-debounce"; + +export interface ValidatedInputProps extends React.ComponentProps { + validation?: { + isValid?: boolean; + error?: string; + hint?: string; + }; + showValidation?: boolean; + onValidate?: (value: string) => { isValid: boolean; error?: string }; +} + +const ValidatedInput = React.forwardRef( + ({ className, validation, showValidation = true, onValidate, onChange, ...props }, ref) => { + const [localValidation, setLocalValidation] = React.useState<{ + isValid?: boolean; + error?: string; + }>({}); + + const debouncedValidate = useDebouncedCallback((value: string) => { + if (onValidate) { + const result = onValidate(value); + setLocalValidation(result); + } + }, 300); + + const handleChange = (e: React.ChangeEvent) => { + onChange?.(e); + if (onValidate && showValidation) { + debouncedValidate(e.target.value); + } + }; + + const validationState = validation || localValidation; + const showSuccess = showValidation && validationState.isValid && props.value; + const showError = showValidation && validationState.error; + + return ( +
+
+ + {showValidation && props.value && ( +
+ {validationState.isValid && ( + + )} + {validationState.error && ( + + )} +
+ )} +
+ + {showValidation && validation?.hint && !validationState.error && ( +

+ {validation.hint} +

+ )} + + {showError && ( +

+ + {validationState.error} +

+ )} +
+ ); + } +); + +ValidatedInput.displayName = "ValidatedInput"; + +export { ValidatedInput }; diff --git a/src/lib/enhancedValidation.ts b/src/lib/enhancedValidation.ts new file mode 100644 index 00000000..9c933baf --- /dev/null +++ b/src/lib/enhancedValidation.ts @@ -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.)', +}; diff --git a/src/lib/glossary.ts b/src/lib/glossary.ts new file mode 100644 index 00000000..f67f518c --- /dev/null +++ b/src/lib/glossary.ts @@ -0,0 +1,390 @@ +/** + * Theme Park Terminology Glossary + * Comprehensive definitions for technical terms used in forms + */ + +export interface GlossaryTerm { + term: string; + category: 'manufacturer' | 'technology' | 'element' | 'component' | 'measurement' | 'type' | 'material'; + definition: string; + example?: string; + relatedTerms?: string[]; +} + +export const glossary: Record = { + // Manufacturers + 'rmc': { + term: 'RMC', + category: 'manufacturer', + definition: 'Rocky Mountain Construction - Manufacturer known for hybrid coasters with steel IBox track on wooden structures', + example: 'Steel Vengeance at Cedar Point', + relatedTerms: ['ibox-track', 'hybrid-coaster'], + }, + 'intamin': { + term: 'Intamin', + category: 'manufacturer', + definition: 'Swiss manufacturer known for record-breaking coasters and innovative launch systems', + example: 'Millennium Force, Top Thrill Dragster', + relatedTerms: ['hydraulic-launch', 'lsm'], + }, + 'b&m': { + term: 'B&M', + category: 'manufacturer', + definition: 'Bolliger & Mabillard - Swiss manufacturer known for smooth, reliable coasters', + example: 'Fury 325, Banshee, GateKeeper', + relatedTerms: ['inverted', 'wing-coaster', 'dive-coaster'], + }, + 'vekoma': { + term: 'Vekoma', + category: 'manufacturer', + definition: 'Dutch manufacturer with wide range from family coasters to intense thrill rides', + example: 'Space Mountain (Disney), Thunderbird (PowerPark)', + }, + 'gerstlauer': { + term: 'Gerstlauer', + category: 'manufacturer', + definition: 'German manufacturer known for compact, intense coasters with vertical lifts', + example: 'Takabisha (steepest drop), Karacho', + relatedTerms: ['euro-fighter'], + }, + 's&s': { + term: 'S&S', + category: 'manufacturer', + definition: 'S&S Worldwide - American manufacturer of compressed-air launch coasters and thrill rides', + example: 'Hypersonic XLC, Screamin\' Swing', + relatedTerms: ['compressed-air-launch'], + }, + + // Launch/Propulsion Systems + 'lsm': { + term: 'LSM Launch', + category: 'technology', + definition: 'Linear Synchronous Motor - Uses electromagnetic propulsion to smoothly accelerate trains', + example: 'Maverick, Taron, Velocicoaster', + relatedTerms: ['lim', 'magnetic-launch'], + }, + 'lim': { + term: 'LIM Launch', + category: 'technology', + definition: 'Linear Induction Motor - Earlier electromagnetic launch technology, less efficient than LSM', + example: 'Flight of Fear, Rock \'n\' Roller Coaster', + relatedTerms: ['lsm'], + }, + 'hydraulic-launch': { + term: 'Hydraulic Launch', + category: 'technology', + definition: 'Uses hydraulic winch system to rapidly accelerate train, capable of extreme speeds', + example: 'Top Thrill Dragster, Kingda Ka (fastest launches)', + relatedTerms: ['intamin'], + }, + 'chain-lift': { + term: 'Chain Lift', + category: 'technology', + definition: 'Traditional lift system using chain and anti-rollback dogs', + example: 'Most traditional wooden and steel coasters', + }, + 'cable-lift': { + term: 'Cable Lift', + category: 'technology', + definition: 'Uses steel cable for faster lift speeds than chain', + example: 'Millennium Force (first major use)', + }, + 'compressed-air-launch': { + term: 'Compressed Air Launch', + category: 'technology', + definition: 'Uses compressed air to launch train, very powerful acceleration', + example: 'Hypersonic XLC, Do-Dodonpa', + relatedTerms: ['s&s'], + }, + + // Coaster Types + 'inverted': { + term: 'Inverted Coaster', + category: 'type', + definition: 'Train runs below the track with feet dangling, track above riders', + example: 'Banshee, Montu, Raptor', + relatedTerms: ['b&m'], + }, + 'wing-coaster': { + term: 'Wing Coaster', + category: 'type', + definition: 'Seats extend to sides of track with nothing above or below riders', + example: 'GateKeeper, The Swarm, X-Flight', + relatedTerms: ['b&m'], + }, + 'dive-coaster': { + term: 'Dive Coaster', + category: 'type', + definition: 'Features wide trains and vertical/near-vertical first drop, often with holding brake', + example: 'Valravn, SheiKra, Griffon', + relatedTerms: ['b&m'], + }, + 'flying-coaster': { + term: 'Flying Coaster', + category: 'type', + definition: 'Riders positioned face-down in flying position', + example: 'Tatsu, Manta, Flying Dinosaur', + relatedTerms: ['b&m', 'vekoma'], + }, + 'hyper-coaster': { + term: 'Hyper Coaster', + category: 'type', + definition: 'Coaster between 200-299 feet tall, focused on airtime', + example: 'Diamondback, Nitro, Apollo\'s Chariot', + relatedTerms: ['giga-coaster', 'airtime'], + }, + 'giga-coaster': { + term: 'Giga Coaster', + category: 'type', + definition: 'Coaster between 300-399 feet tall', + example: 'Millennium Force, Fury 325, Leviathan', + relatedTerms: ['hyper-coaster', 'strata-coaster'], + }, + 'strata-coaster': { + term: 'Strata Coaster', + category: 'type', + definition: 'Coaster 400+ feet tall', + example: 'Top Thrill Dragster, Kingda Ka', + relatedTerms: ['giga-coaster'], + }, + 'hybrid-coaster': { + term: 'Hybrid Coaster', + category: 'type', + definition: 'Steel track on wooden support structure', + example: 'Steel Vengeance, Twisted Colossus', + relatedTerms: ['rmc', 'ibox-track'], + }, + 'euro-fighter': { + term: 'Euro-Fighter', + category: 'type', + definition: 'Compact Gerstlauer coaster with vertical lift and beyond-vertical drop', + example: 'Takabisha, Saw: The Ride', + relatedTerms: ['gerstlauer'], + }, + + // Track Materials + 'ibox-track': { + term: 'IBox Track', + category: 'material', + definition: 'RMC\'s steel box-beam track system used on hybrid coasters, allows extreme elements', + example: 'Steel Vengeance, Iron Rattler', + relatedTerms: ['rmc', 'hybrid-coaster', 'topper-track'], + }, + 'topper-track': { + term: 'Topper Track', + category: 'material', + definition: 'RMC\'s steel plate topper on wooden track for smoother wooden coaster experience', + example: 'Outlaw Run, Lightning Rod', + relatedTerms: ['rmc', 'ibox-track'], + }, + + // Restraint Types + 'otsr': { + term: 'OTSR', + category: 'component', + definition: 'Over-The-Shoulder Restraint - Safety harness that goes over shoulders and locks at waist', + example: 'Used on most inverting coasters', + relatedTerms: ['vest-restraint', 'lap-bar'], + }, + 'lap-bar': { + term: 'Lap Bar', + category: 'component', + definition: 'Restraint that only crosses the lap/thighs, offers more freedom', + example: 'Millennium Force, most airtime-focused rides', + relatedTerms: ['otsr', 't-bar'], + }, + 't-bar': { + term: 'T-Bar', + category: 'component', + definition: 'T-shaped lap bar restraint, common on Intamin hyper coasters', + example: 'Intimidator 305, Skyrush', + relatedTerms: ['lap-bar'], + }, + 'vest-restraint': { + term: 'Vest Restraint', + category: 'component', + definition: 'Soft vest-style over-shoulder restraint, more comfortable than traditional OTSR', + example: 'GateKeeper, Valravn (B&M)', + relatedTerms: ['otsr'], + }, + 'shin-bar': { + term: 'Shin Bar', + category: 'component', + definition: 'Additional restraint that holds shins in place, used on some intense rides', + example: 'Flying coasters, some Vekoma rides', + }, + + // Elements + 'airtime': { + term: 'Airtime', + category: 'element', + definition: 'Negative G-forces that create sensation of floating or being lifted from seat', + example: 'Camelback hills, speed hills', + relatedTerms: ['ejector-airtime', 'floater-airtime', 'hangtime'], + }, + 'ejector-airtime': { + term: 'Ejector Airtime', + category: 'element', + definition: 'Strong negative Gs that forcefully lift riders from seats', + example: 'El Toro, Skyrush airtime hills', + relatedTerms: ['airtime'], + }, + 'floater-airtime': { + term: 'Floater Airtime', + category: 'element', + definition: 'Gentle negative Gs that create sustained floating sensation', + example: 'B&M hyper coasters', + relatedTerms: ['airtime'], + }, + 'hangtime': { + term: 'Hangtime', + category: 'element', + definition: 'Suspension in mid-air during inversion, typically at apex of element', + example: 'Zero-g rolls, inversions on dive coasters', + relatedTerms: ['airtime', 'inversion'], + }, + 'inversion': { + term: 'Inversion', + category: 'element', + definition: 'Element where riders are turned upside down (≥90 degrees from upright)', + example: 'Loops, corkscrews, barrel rolls', + relatedTerms: ['zero-g-roll', 'corkscrew', 'loop'], + }, + 'zero-g-roll': { + term: 'Zero-G Roll', + category: 'element', + definition: 'Heartline inversion with sustained weightlessness', + example: 'Common on Intamin and B&M coasters', + relatedTerms: ['inversion', 'hangtime'], + }, + 'corkscrew': { + term: 'Corkscrew', + category: 'element', + definition: 'Inversion where track twists 360 degrees while moving forward', + example: 'Classic Arrow element', + relatedTerms: ['inversion'], + }, + 'loop': { + term: 'Vertical Loop', + category: 'element', + definition: 'Full 360-degree vertical circle', + example: 'Classic clothoid loop shape', + relatedTerms: ['inversion'], + }, + 'dive-loop': { + term: 'Dive Loop', + category: 'element', + definition: 'Half loop up, half corkscrew down', + example: 'Common on B&M coasters', + relatedTerms: ['immelmann', 'inversion'], + }, + 'immelmann': { + term: 'Immelmann', + category: 'element', + definition: 'Half loop up, half roll out (opposite of dive loop)', + example: 'Named after WWI pilot maneuver', + relatedTerms: ['dive-loop', 'inversion'], + }, + 'cobra-roll': { + term: 'Cobra Roll', + category: 'element', + definition: 'Double inversion creating S-shape, reversing direction', + example: 'Common on Vekoma and B&M loopers', + relatedTerms: ['inversion'], + }, + 'heartline-roll': { + term: 'Heartline Roll', + category: 'element', + definition: 'Barrel roll rotating around rider\'s heartline for smooth inversion', + example: 'Maverick, many Intamin coasters', + relatedTerms: ['zero-g-roll', 'inversion'], + }, + + // Technical Terms + 'mcbr': { + term: 'MCBR', + category: 'component', + definition: 'Mid-Course Brake Run - Safety brake zone that divides track into blocks', + example: 'Allows multiple trains to operate safely', + relatedTerms: ['block-section'], + }, + 'block-section': { + term: 'Block Section', + category: 'component', + definition: 'Track section that only one train can occupy at a time for safety', + example: 'Station, lift hill, brake runs', + relatedTerms: ['mcbr'], + }, + 'trim-brake': { + term: 'Trim Brake', + category: 'component', + definition: 'Brake that slows train slightly to control speed', + example: 'Often on hills or before elements', + }, + 'transfer-track': { + term: 'Transfer Track', + category: 'component', + definition: 'Movable track section for adding/removing trains from circuit', + example: 'Allows storage of extra trains', + }, + 'anti-rollback': { + term: 'Anti-Rollback', + category: 'component', + definition: 'Safety device preventing train from rolling backward on lift', + example: 'Creates "clicking" sound on chain lifts', + }, + + // Measurements + 'g-force': { + term: 'G-Force', + category: 'measurement', + definition: 'Force of gravity felt by riders. 1G = normal gravity, positive = pushed into seat, negative = lifted from seat', + example: '4.5G on intense loops, -1.5G on airtime hills', + }, + 'kilometers-per-hour': { + term: 'km/h', + category: 'measurement', + definition: 'Speed measurement in kilometers per hour (metric)', + example: '193 km/h = 120 mph', + }, + 'meters': { + term: 'Meters', + category: 'measurement', + definition: 'Length/height measurement (metric). 1 meter ≈ 3.28 feet', + example: '94 meters = 310 feet', + }, +}; + +/** + * Get glossary term by key (normalized) + */ +export function getGlossaryTerm(term: string): GlossaryTerm | undefined { + const key = term.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + return glossary[key]; +} + +/** + * Search glossary by query + */ +export function searchGlossary(query: string): GlossaryTerm[] { + const lowerQuery = query.toLowerCase(); + return Object.values(glossary).filter(term => + term.term.toLowerCase().includes(lowerQuery) || + term.definition.toLowerCase().includes(lowerQuery) || + term.example?.toLowerCase().includes(lowerQuery) + ); +} + +/** + * Get all terms in a category + */ +export function getTermsByCategory(category: GlossaryTerm['category']): GlossaryTerm[] { + return Object.values(glossary).filter(term => term.category === category); +} + +/** + * Get all categories + */ +export function getAllCategories(): GlossaryTerm['category'][] { + return ['manufacturer', 'technology', 'element', 'component', 'measurement', 'type', 'material']; +}