Files
textbee/web/components/shared/footer.tsx
isra el 022a6bd2a6 fix: repair form labelling and navigation landmarks
None of the three password fields declared autoComplete, so password
managers could not tell them apart and would fill the saved password
into the wrong box. They now declare current-password and new-password.

The delete-account dialog, the most destructive action in the app, had
a label whose htmlFor pointed at an id the textarea never had, and an
email confirmation field with no label at all. Both are now wired, and
the controls moved out of DialogDescription, which Radix targets with
aria-describedby: opening the dialog previously read the labels and
placeholders back as one long description string.

Validation errors are now tied to their inputs via aria-describedby and
announced with role=alert instead of appearing silently.

confirmPassword required 4 characters while newPassword required 8, so
a five-character confirmation reported "Passwords must match" rather
than the actual length problem.

Five icon-only buttons had no accessible name: copy API key, copy
device ID, the API key menu, and both promo code copy buttons.

Four of the five nav landmarks were unlabelled, leaving them
indistinguishable in a screen reader's landmark list. Added a
skip-to-content link, since keyboard users otherwise tab the whole
sidebar on every page.

The delete-account label guard was confirmed to fail against the
previous markup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 02:00:11 +03:00

72 lines
2.8 KiB
TypeScript

import { Routes } from '@/config/routes'
import { Activity } from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
// A logged-in user is already converted, so the app footer stays a single slim
// bar rather than the marketing site's multi-column link farm. It borrows that
// footer's visual language (muted surface, muted-to-foreground link hovers,
// green status pill) so the two still read as one product.
const links = [
{ label: 'Quick start', href: Routes.quickstart },
{ label: 'Download app', href: Routes.downloadAndroidApp },
{ label: 'Contribute', href: Routes.contribute },
{ label: 'Privacy', href: Routes.privacyPolicy },
{ label: 'Terms', href: Routes.termsOfService },
{ label: 'Refund', href: Routes.refundPolicy },
]
const linkClass =
'text-sm text-muted-foreground transition-colors hover:text-foreground'
export default function Footer() {
return (
<footer className='border-t border-border bg-muted/30'>
{/* Left-aligned on mobile: centred links in a single column read as a
ragged stack with no common edge to scan down. */}
<div className='mx-auto flex max-w-7xl flex-col items-start gap-4 px-4 py-6 sm:items-center sm:px-6 md:flex-row md:justify-between lg:px-8'>
<div className='flex items-center gap-2'>
<Image
src='/images/logo.png'
alt='textbee logo'
width={20}
height={20}
className='h-5 w-5 rounded-full bg-white'
/>
<span className='text-sm text-muted-foreground'>
© {new Date().getFullYear()} textbee.dev
</span>
</div>
{/* Stacked on mobile: wrapped inline links produced a ragged two-line
block that was hard to scan and gave small tap targets. */}
<nav
aria-label='Footer'
className='flex w-full flex-col items-start gap-3 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-center sm:gap-x-5 sm:gap-y-2'
>
{links.map((link) => (
<Link
key={link.label}
href={link.href}
target='_blank'
rel='noopener noreferrer'
className={linkClass}
>
{link.label}
</Link>
))}
<Link
href={Routes.statusPage}
target='_blank'
rel='nofollow noopener noreferrer'
className='inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2.5 py-1 text-sm font-medium text-green-700 transition-colors hover:bg-green-100 dark:bg-green-900/20 dark:text-green-400 dark:hover:bg-green-900/30'
>
<Activity className='h-3.5 w-3.5 text-green-500' />
Status
</Link>
</nav>
</div>
</footer>
)
}