mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 04:11:12 -05:00
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:
56
src/components/ui/term-tooltip.tsx
Normal file
56
src/components/ui/term-tooltip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
87
src/components/ui/validated-input.tsx
Normal file
87
src/components/ui/validated-input.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user