- New /courses page with 5 course cards (Interview Ready, Self Assessment, Payroll, QuickBooks, Xero) with thumbnail images, pricing, includes checklists, and enrol CTAs - Added course images to public/courses/ and Assets/ - Contact page: updated email, phone (07440 594192), address (Suite 29 Beaufort Court E14 9XL), hours, and added Courses option to the "I'm interested in" select field - Header: added Courses to desktop nav and mobile dock (GraduationCap icon) between Services and Blog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
2.5 KiB
TypeScript
98 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import { forwardRef } from 'react';
|
|
import Link from 'next/link';
|
|
import { Spinner } from './Spinner';
|
|
import { ArrowRightIcon } from './icons';
|
|
|
|
type Variant = 'primary' | 'secondary' | 'ghost';
|
|
type Size = 'sm' | 'md' | 'lg';
|
|
|
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: Variant;
|
|
size?: Size;
|
|
loading?: boolean;
|
|
trailingArrow?: boolean;
|
|
leadingIcon?: React.ReactNode;
|
|
href?: string;
|
|
}
|
|
|
|
const variants: Record<Variant, string> = {
|
|
primary:
|
|
'bg-emerald text-white hover:bg-emerald-dark active:bg-emerald-deeper ' +
|
|
'focus-visible:ring-emerald/50 shadow-sm',
|
|
secondary:
|
|
'border border-emerald text-emerald bg-transparent hover:bg-emerald-mist ' +
|
|
'focus-visible:ring-emerald/50',
|
|
ghost: 'text-emerald bg-transparent hover:bg-emerald-mist ' + 'focus-visible:ring-emerald/50',
|
|
};
|
|
|
|
const sizes: Record<Size, string> = {
|
|
sm: 'h-9 px-4 text-sm gap-1.5 rounded-pill',
|
|
md: 'h-11 px-6 text-base gap-2 rounded-pill',
|
|
lg: 'h-14 px-8 text-lg gap-2.5 rounded-pill',
|
|
};
|
|
|
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
(
|
|
{
|
|
variant = 'primary',
|
|
size = 'md',
|
|
loading = false,
|
|
trailingArrow = false,
|
|
leadingIcon,
|
|
children,
|
|
className = '',
|
|
disabled,
|
|
href,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const arrowSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16;
|
|
|
|
const classes = [
|
|
'inline-flex cursor-pointer items-center justify-center font-medium',
|
|
'transition-all duration-200',
|
|
'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
|
'active:scale-[0.98]',
|
|
'disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100',
|
|
variants[variant],
|
|
sizes[size],
|
|
className,
|
|
].join(' ');
|
|
|
|
const content = (
|
|
<>
|
|
{loading ? (
|
|
<Spinner size={size === 'lg' ? 'md' : 'sm'} />
|
|
) : leadingIcon ? (
|
|
leadingIcon
|
|
) : null}
|
|
{children}
|
|
{!loading && trailingArrow && (
|
|
<ArrowRightIcon
|
|
size={arrowSize}
|
|
className="shrink-0 transition-transform duration-200 group-hover:translate-x-0.5"
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
if (href) {
|
|
return (
|
|
<Link href={href} className={classes}>
|
|
{content}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button ref={ref} disabled={disabled || loading} className={classes} {...props}>
|
|
{content}
|
|
</button>
|
|
);
|
|
},
|
|
);
|
|
|
|
Button.displayName = 'Button';
|