Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/components/Loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ interface LoaderProps {

export function Loader({ size = 24, className, text }: LoaderProps) {
return (
<div className="flex items-center justify-center gap-2">
<div role="status" aria-label={text ?? 'Loading'} className="flex items-center justify-center gap-2">
<Loader2
aria-hidden="true"
size={size}
className={cn("animate-spin", className)}
/>
Expand All @@ -31,17 +32,21 @@ export function LoaderPending(){
// Other variants...
export function LoaderFullPage({ text = 'Loading...' }: { text?: string }) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-50">
<div role="status" aria-label={text} className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-50">
<div className="flex flex-col items-center gap-4">
<Loader2 size={48} className="animate-spin text-primary" />
<Loader2 aria-hidden="true" size={48} className="animate-spin text-primary" />
<p className="text-lg font-medium">{text}</p>
</div>
</div>
)
}

export function LoaderInline({ size = 16 }: { size?: number }) {
return <Loader2 size={size} className="animate-spin" />
return (
<span role="status" aria-label="Loading">
<Loader2 aria-hidden="true" size={size} className="animate-spin" />
</span>
)
}

export function LoaderOverlay({ text }: { text?: string }) {
Expand Down
20 changes: 11 additions & 9 deletions src/components/form/ConverterPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,17 @@ export function ConverterPage<T extends ConverterFormBase>({
<form.ConvertActions onReset={handleReset} />
</form.AppForm>

{output && (
<FormTextArea
name="output"
label={config.outputLabel(mode)}
value={output}
readOnly
className="font-mono"
/>
)}
<div aria-live="polite" aria-atomic="true">
{output && (
<FormTextArea
name="output"
label={config.outputLabel(mode)}
value={output}
readOnly
className="font-mono"
/>
)}
</div>
</form>
</div>
)
Expand Down
15 changes: 0 additions & 15 deletions src/components/form/FieldErrorMessage.tsx

This file was deleted.

27 changes: 13 additions & 14 deletions src/components/form/SelectField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormSelect } from './FormSelect'
import { FieldErrorMessage } from './FieldErrorMessage'
import { useFieldContext } from '@/hooks/form'
import { formatFieldErrors } from '@/lib/errors'
import type { SelectOption } from '@/lib/converter-configs'

interface SelectFieldProps {
Expand All @@ -11,19 +11,18 @@ interface SelectFieldProps {
export function SelectField({ label, options }: SelectFieldProps) {
const field = useFieldContext<string>()

const shouldShow = field.state.meta.isTouched || field.state.meta.isBlurred || field.form.state.isSubmitted
const errs = field.state.meta.errors ?? []
const errorMessage = shouldShow && errs.length > 0 ? formatFieldErrors(errs) : undefined

return (
<>
<FormSelect
name={field.name}
label={label}
value={field.state.value}
onChange={(value: string) => field.setValue(value)}
options={options}
/>
<FieldErrorMessage
meta={field.state.meta}
showWhenSubmitted={field.form.state.isSubmitted}
/>
</>
<FormSelect
name={field.name}
label={label}
value={field.state.value}
onChange={(value: string) => field.setValue(value)}
options={options}
errorMessage={errorMessage}
/>
)
}
35 changes: 17 additions & 18 deletions src/components/form/TextAreaField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormTextArea } from './FormTextArea'
import { FieldErrorMessage } from './FieldErrorMessage'
import { useFieldContext } from '@/hooks/form'
import { formatFieldErrors } from '@/lib/errors'

interface TextAreaFieldProps {
label: string
Expand All @@ -19,23 +19,22 @@ export function TextAreaField({
}: TextAreaFieldProps) {
const field = useFieldContext<string>()

const shouldShow = field.state.meta.isTouched || field.state.meta.isBlurred || field.form.state.isSubmitted
const errs = field.state.meta.errors ?? []
const errorMessage = shouldShow && errs.length > 0 ? formatFieldErrors(errs) : undefined

return (
<>
<FormTextArea
ref={registerRef?.(field.name)}
name={field.name}
label={label}
placeholder={placeholder}
rows={rows}
isRequired
value={field.state.value}
onChange={(value) => field.handleChange(value)}
className={className}
/>
<FieldErrorMessage
meta={field.state.meta}
showWhenSubmitted={field.form.state.isSubmitted}
/>
</>
<FormTextArea
ref={registerRef?.(field.name)}
name={field.name}
label={label}
placeholder={placeholder}
rows={rows}
isRequired
value={field.state.value}
onChange={(value) => field.handleChange(value)}
className={className}
errorMessage={errorMessage}
/>
)
}
1 change: 0 additions & 1 deletion src/components/form/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { FormSelect } from './FormSelect'
export { FormTextArea } from './FormTextArea'
export { FormButton } from './FormButton'
export { FieldErrorMessage } from './FieldErrorMessage'
export { ConverterPage } from './ConverterPage'
13 changes: 8 additions & 5 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,22 @@ const Home = () => {
<div className="flex flex-col md:flex-row gap-4">
{/* Search Input */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<Search aria-hidden="true" className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<input
type="text"
placeholder="Search tools..."
aria-label="Search tools"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
/>
</div>

{/* Category Filter */}
<div className="flex gap-2 flex-wrap md:flex-nowrap">
<div role="group" aria-label="Filter by category" className="flex gap-2 flex-wrap md:flex-nowrap">
<button
onClick={() => setSelectedCategory('All')}
aria-pressed={selectedCategory === 'All'}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
selectedCategory === 'All'
? 'bg-primary text-primary-foreground'
Expand All @@ -64,6 +66,7 @@ const Home = () => {
<button
key={category}
onClick={() => setSelectedCategory(category)}
aria-pressed={selectedCategory === category}
className={`px-4 py-2 rounded-lg font-medium transition-all whitespace-nowrap ${
selectedCategory === category
? 'bg-primary text-primary-foreground'
Expand Down Expand Up @@ -139,7 +142,7 @@ const ToolCard = ({ tool }: ToolCardProps) => {
>
{/* Icon */}
<div className="mb-4 inline-flex rounded-lg bg-primary/10 p-3 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
<Icon className="h-6 w-6" />
<Icon aria-hidden="true" className="h-6 w-6" />
</div>

{/* Content */}
Expand All @@ -155,7 +158,7 @@ const ToolCard = ({ tool }: ToolCardProps) => {
<span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-1 rounded">
{tool.category}
</span>
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
<ArrowRight aria-hidden="true" className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
</div>

{/* Hover Effect Gradient */}
Expand All @@ -173,7 +176,7 @@ interface FeatureCardProps {
const FeatureCard = ({ title, description, icon }: FeatureCardProps) => {
return (
<div className="text-center p-6 rounded-xl border border-border bg-card/50 backdrop-blur">
<div className="text-4xl mb-4">{icon}</div>
<div className="text-4xl mb-4" role="img" aria-label={title}>{icon}</div>
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
Expand Down