Bug fixes, SEO, performance and accessibility pass #1
5 changed files with 62 additions and 29 deletions
|
|
@ -24,6 +24,8 @@ export function MobileNavbar() {
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
aria-label={item.label}
|
||||||
|
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
isActive(item.href) ? 'text-accent' : 'text-fg-dim hover:text-fg',
|
isActive(item.href) ? 'text-accent' : 'text-fg-dim hover:text-fg',
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,11 @@ export function NavLinks() {
|
||||||
{item.label}
|
{item.label}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
) : (
|
) : (
|
||||||
<Link href={item.href} className="flex items-center gap-2 px-2 py-1">
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="flex items-center gap-2 px-2 py-1"
|
||||||
|
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||||
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ export async function Sidebar() {
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto flex gap-4 mx-auto">
|
<div className="mt-auto flex gap-4 mx-auto">
|
||||||
{socialLinks.map(({ icon, url }) => (
|
{socialLinks.map(({ icon, url }) => (
|
||||||
<ExternalLink key={url} href={url} skipIcon>
|
<ExternalLink key={url} href={url} skipIcon aria-label={icon.title}>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
dangerouslySetInnerHTML={{ __html: icon.svg }}
|
dangerouslySetInnerHTML={{ __html: icon.svg }}
|
||||||
className="inline-block w-6 h-6 mr-1 fill-fg-dim hover:fill-accent transition-colors"
|
className="inline-block w-6 h-6 mr-1 fill-fg-dim hover:fill-accent transition-colors"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,9 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 mx-auto">
|
<div className="flex gap-4 mx-auto">
|
||||||
{networks.map(({ name, icon, href }) => (
|
{networks.map(({ name, icon, href }) => (
|
||||||
<ExternalLink key={name} href={href} skipIcon>
|
<ExternalLink key={name} href={href} skipIcon aria-label={`Share on ${name}`}>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
dangerouslySetInnerHTML={{ __html: icon.svg }}
|
dangerouslySetInnerHTML={{ __html: icon.svg }}
|
||||||
className="inline-block w-6 h-6 mr-1 fill-current hover:text-accent transition-colors"
|
className="inline-block w-6 h-6 mr-1 fill-current hover:text-accent transition-colors"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,66 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { ButtonHTMLAttributes, useRef } from 'react'
|
import { ButtonHTMLAttributes, useEffect, useState } from 'react'
|
||||||
import { Copy } from 'lucide-react'
|
import { Check, Copy } from 'lucide-react'
|
||||||
import { cn } from '@/utils/cn'
|
import { cn } from '@/utils/cn'
|
||||||
|
|
||||||
type CopyButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
type CopyButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
textToCopy: string
|
textToCopy: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps) {
|
type CopyState = 'idle' | 'copied' | 'failed'
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null)
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
const labels: Record<CopyState, string> = {
|
||||||
navigator.clipboard.writeText(textToCopy)
|
idle: 'Copy code',
|
||||||
const button = buttonRef.current
|
copied: 'Copied!',
|
||||||
if (button) {
|
failed: 'Copy failed',
|
||||||
const original = button.innerHTML
|
}
|
||||||
button.innerHTML = 'Copied!'
|
|
||||||
button.disabled = true
|
export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps) {
|
||||||
setTimeout(() => {
|
const [state, setState] = useState<CopyState>('idle')
|
||||||
button.innerHTML = original
|
|
||||||
button.disabled = false
|
useEffect(() => {
|
||||||
}, 2000)
|
if (state === 'idle') return
|
||||||
|
const timeout = setTimeout(() => setState('idle'), 2000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(textToCopy)
|
||||||
|
setState('copied')
|
||||||
|
} catch {
|
||||||
|
setState('failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-fg-dim hover:text-fg transition-colors js-only text-xs cursor-pointer disabled:cursor-default disabled:hover:text-fg-dim',
|
'text-fg-dim hover:text-fg transition-colors js-only text-xs cursor-pointer disabled:cursor-default disabled:hover:text-fg-dim',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
ref={buttonRef}
|
disabled={state !== 'idle'}
|
||||||
|
aria-label={labels.idle}
|
||||||
|
title={labels.idle}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
{state === 'idle' ? (
|
||||||
<Copy className="w-4 h-4" />
|
<Copy className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
{state === 'copied' && <Check className="w-4 h-4" />}
|
||||||
|
{labels[state]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{/* Outside the button: a button's children are presentational, so they aren't announced */}
|
||||||
|
<span className="sr-only" aria-live="polite">
|
||||||
|
{state === 'idle' ? '' : labels[state]}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue