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

@@ -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 }:
<MapPin className="w-5 h-5" />
{isEditing ? 'Edit Park' : 'Create New Park'}
</CardTitle>
<SubmissionHelpDialog type="park" variant="icon" />
<div className="flex gap-2">
<TerminologyDialog />
<SubmissionHelpDialog type="park" variant="icon" />
</div>
</div>
</CardHeader>
<CardContent>
<TooltipProvider>
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
{/* Basic Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -724,6 +731,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
)}
</div>
</form>
</TooltipProvider>
{/* Operator Modal */}
<Dialog open={isOperatorModalOpen} onOpenChange={setIsOperatorModalOpen}>

View File

@@ -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 }:
<Zap className="w-5 h-5" />
{isEditing ? 'Edit Ride' : 'Create New Ride'}
</CardTitle>
<SubmissionHelpDialog type="ride" variant="icon" />
<div className="flex gap-2">
<TerminologyDialog />
<SubmissionHelpDialog type="ride" variant="icon" />
</div>
</div>
</CardHeader>
<CardContent>
@@ -851,17 +857,15 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label>Track Material(s)</Label>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>Material used for the track. Select multiple if hybrid (e.g., wood track with steel supports).</p>
</TooltipContent>
</Tooltip>
<Label>
<TermTooltip term="ibox-track" showIcon={false}>
Track Material(s)
</TermTooltip>
</Label>
</div>
<p className="text-sm text-muted-foreground">Select all materials used in the track</p>
<p className="text-sm text-muted-foreground">
Common: <TermTooltip term="ibox-track" inline>Steel</TermTooltip>, Wood, <TermTooltip term="hybrid-coaster" inline>Hybrid (RMC IBox)</TermTooltip>
</p>
<div className="grid grid-cols-2 gap-3">
{TRACK_MATERIALS.map((material) => (
<div key={material.value} className="flex items-center space-x-2">
@@ -888,16 +892,10 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label>Support Material(s)</Label>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>Material used for the support structure. Can be different from track material (e.g., wood track on steel supports).</p>
</TooltipContent>
</Tooltip>
</div>
<p className="text-sm text-muted-foreground">Select all materials used in the supports</p>
<p className="text-sm text-muted-foreground">
Materials used for support structure (can differ from track)
</p>
<div className="grid grid-cols-2 gap-3">
{SUPPORT_MATERIALS.map((material) => (
<div key={material.value} className="flex items-center space-x-2">
@@ -923,23 +921,15 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label>Propulsion Method(s)</Label>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-semibold mb-1">Common methods:</p>
<ul className="text-xs space-y-1">
<li> <strong>LSM Launch:</strong> Linear Synchronous Motor (smooth, modern)</li>
<li> <strong>Chain Lift:</strong> Traditional lift hill</li>
<li> <strong>Hydraulic Launch:</strong> Fast, powerful (e.g., Kingda Ka)</li>
<li> <strong>Gravity:</strong> Free-fall or terrain-based</li>
</ul>
</TooltipContent>
</Tooltip>
<Label>
<TermTooltip term="lsm" showIcon={false}>
Propulsion Method(s)
</TermTooltip>
</Label>
</div>
<p className="text-sm text-muted-foreground">Select all propulsion methods used</p>
<p className="text-sm text-muted-foreground">
Common: <TermTooltip term="lsm" inline>LSM Launch</TermTooltip>, <TermTooltip term="chain-lift" inline>Chain Lift</TermTooltip>, <TermTooltip term="hydraulic-launch" inline>Hydraulic Launch</TermTooltip>
</p>
<div className="grid grid-cols-2 gap-3">
{PROPULSION_METHODS.map((method) => (
<div key={method.value} className="flex items-center space-x-2">

View File

@@ -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,

View File

@@ -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) => (
<div key={term.term} className="p-4 border rounded-lg space-y-2 hover:bg-muted/50 transition-colors">
<div className="flex items-start justify-between gap-2">
<h4 className="font-semibold">{term.term}</h4>
<Badge variant="secondary" className="text-xs shrink-0">
{term.category.replace('-', ' ')}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{term.definition}</p>
{term.example && (
<p className="text-xs text-muted-foreground italic">
<span className="font-medium">Example:</span> {term.example}
</p>
)}
{term.relatedTerms && term.relatedTerms.length > 0 && (
<div className="flex flex-wrap gap-1 pt-1">
<span className="text-xs text-muted-foreground">Related:</span>
{term.relatedTerms.map(rt => (
<Badge key={rt} variant="outline" className="text-xs">
{rt.replace(/-/g, ' ')}
</Badge>
))}
</div>
)}
</div>
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<BookOpen className="w-4 h-4 mr-2" />
Terminology
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Theme Park Terminology Reference</DialogTitle>
<DialogDescription>
Quick reference for technical terms, manufacturers, and ride types
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search terminology..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{/* Results */}
{searchQuery ? (
<ScrollArea className="h-[400px]">
<div className="space-y-3">
{searchResults.length > 0 ? (
searchResults.map(renderTermCard)
) : (
<div className="text-center py-12 text-muted-foreground">
No terms found matching "{searchQuery}"
</div>
)}
</div>
</ScrollArea>
) : (
<Tabs defaultValue="manufacturer" className="w-full">
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-7">
{categories.map(cat => (
<TabsTrigger key={cat} value={cat} className="text-xs">
{cat === 'manufacturer' ? 'Mfg.' :
cat === 'technology' ? 'Tech' :
cat === 'measurement' ? 'Units' :
cat.charAt(0).toUpperCase() + cat.slice(1).substring(0, 4)}
</TabsTrigger>
))}
</TabsList>
{categories.map(cat => {
const terms = getTermsByCategory(cat);
return (
<TabsContent key={cat} value={cat}>
<ScrollArea className="h-[400px]">
<div className="space-y-3 pr-4">
<div className="flex items-center gap-2 pb-2 border-b">
<h3 className="font-semibold capitalize">
{cat.replace('-', ' ')}
</h3>
<Badge variant="secondary">{terms.length} terms</Badge>
</div>
{terms.map(renderTermCard)}
</div>
</ScrollArea>
</TabsContent>
);
})}
</Tabs>
)}
</div>
<div className="flex items-center gap-2 pt-4 border-t text-xs text-muted-foreground">
<Badge variant="outline" className="text-xs">Tip</Badge>
<span>Hover over underlined terms in forms to see quick definitions</span>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -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 (
<Tooltip>
<TooltipTrigger asChild>
<span className={cn(
"inline-flex items-center gap-1",
inline && "underline decoration-dotted cursor-help"
)}>
{children}
{showIcon && (
<HelpCircle className="inline-block w-3 h-3 text-muted-foreground" />
)}
</span>
</TooltipTrigger>
<TooltipContent className="max-w-xs" side="top">
<div className="space-y-1">
<div className="font-semibold text-sm">{glossaryEntry.term}</div>
<p className="text-xs text-muted-foreground capitalize">
{glossaryEntry.category.replace('-', ' ')}
</p>
<p className="text-sm">{glossaryEntry.definition}</p>
{glossaryEntry.example && (
<p className="text-xs text-muted-foreground italic pt-1">
Example: {glossaryEntry.example}
</p>
)}
{glossaryEntry.relatedTerms && glossaryEntry.relatedTerms.length > 0 && (
<p className="text-xs text-muted-foreground pt-1">
See also: {glossaryEntry.relatedTerms.map(t =>
getGlossaryTerm(t)?.term || t
).join(', ')}
</p>
)}
</div>
</TooltipContent>
</Tooltip>
);
}

View File

@@ -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<typeof Input> {
validation?: {
isValid?: boolean;
error?: string;
hint?: string;
};
showValidation?: boolean;
onValidate?: (value: string) => { isValid: boolean; error?: string };
}
const ValidatedInput = React.forwardRef<HTMLInputElement, ValidatedInputProps>(
({ 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<HTMLInputElement>) => {
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 (
<div className="space-y-1">
<div className="relative">
<Input
ref={ref}
className={cn(
showError && "border-destructive focus-visible:ring-destructive",
showSuccess && "border-green-500 focus-visible:ring-green-500",
"pr-8",
className
)}
onChange={handleChange}
{...props}
/>
{showValidation && props.value && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
{validationState.isValid && (
<Check className="w-4 h-4 text-green-500" />
)}
{validationState.error && (
<AlertCircle className="w-4 h-4 text-destructive" />
)}
</div>
)}
</div>
{showValidation && validation?.hint && !validationState.error && (
<p className="text-xs text-muted-foreground">
{validation.hint}
</p>
)}
{showError && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
{validationState.error}
</p>
)}
</div>
);
}
);
ValidatedInput.displayName = "ValidatedInput";
export { ValidatedInput };

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.)',
};

390
src/lib/glossary.ts Normal file
View File

@@ -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<string, GlossaryTerm> = {
// 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'];
}