feat: add courses page, real contact details, Courses to navigation

- 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>
This commit is contained in:
Vadym Samoilenko 2026-02-23 13:25:16 +00:00
parent 9d4bb7ad34
commit 7bb25f9c59
20 changed files with 470 additions and 98 deletions

View file

@ -1,6 +1,6 @@
{
"enabledPlugins": {
"frontend-design@claude-code-plugins": false,
"frontend-design@claude-code-plugins": true,
"feature-dev@claude-code-plugins": false
}
}

View file

@ -31,7 +31,8 @@
"Bash(node_modules/.bin/eslint:*)",
"Bash(node --input-type=module:*)",
"Bash(python3:*)",
"Bash(ls:*)"
"Bash(ls:*)",
"WebFetch(domain:axilaccountants.co.uk)"
]
}
}

BIN
Assets/CV.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
Assets/payroll-1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
Assets/quickbooks.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
Assets/tax.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
Assets/xero.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
public/courses/cv.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
public/courses/payroll.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
public/courses/tax.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
public/courses/xero.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View file

@ -13,8 +13,8 @@ export const metadata: Metadata = {
const CONTACT_DETAILS = [
{
label: 'Email',
value: 'hello@axilaccountants.co.uk',
href: 'mailto:hello@axilaccountants.co.uk',
value: 'info@axilaccountants.co.uk',
href: 'mailto:info@axilaccountants.co.uk',
icon: (
<svg
width="20"
@ -33,8 +33,8 @@ const CONTACT_DETAILS = [
},
{
label: 'Phone',
value: '0207 123 4567',
href: 'tel:+442071234567',
value: '07440 594192',
href: 'tel:+447440594192',
icon: (
<svg
width="20"
@ -52,8 +52,8 @@ const CONTACT_DETAILS = [
},
{
label: 'Address',
value: '12 Finsbury Square, London, EC2A 1AB',
href: 'https://maps.google.com',
value: 'Suite 29, Beaufort Court, Admirals Way, London E14 9XL',
href: 'https://maps.google.com/?q=Beaufort+Court+Admirals+Way+London+E14+9XL',
icon: (
<svg
width="20"
@ -72,7 +72,7 @@ const CONTACT_DETAILS = [
},
{
label: 'Hours',
value: 'MonFri, 9am6pm GMT',
value: 'MonFri: 9am5pm · Sat: by appointment · Sun: closed',
href: null,
icon: (
<svg
@ -215,6 +215,7 @@ export default function ContactPage() {
<option value="payroll">Payroll</option>
<option value="vat-returns">VAT Returns</option>
<option value="all">Full accounting package</option>
<option value="courses">Courses</option>
<option value="other">Other / Not sure</option>
</select>
</div>

331
src/app/courses/page.tsx Normal file
View file

@ -0,0 +1,331 @@
import type { Metadata } from 'next';
import Image from 'next/image';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { BeamButton } from '@/components/ui/BeamButton';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { CheckCircleIcon } from '@/components/ui/icons';
export const metadata: Metadata = {
title: 'Courses — Axil Accountants',
description:
'Practical accounting courses for UK learners. Self Assessment, Payroll, QuickBooks, Xero and interview preparation — taught by qualified accountants.',
};
const COURSES = [
{
id: 'interview-ready',
image: '/courses/cv.jpeg',
badge: '01',
title: 'Interview Ready',
subtitle: 'Your first UK accounting role',
price: '£129',
level: 'Beginner',
duration: 'Self-paced',
isNew: false,
desc: 'Designed for aspiring accountants who are ready to enter the workforce. Learn exactly what UK employers expect, how to present your experience confidently, and how to succeed at every stage — from CV to offer letter.',
includes: [
'CV & cover letter guidance',
'Mock interview practice',
'UK accounting terminology',
'What employers look for',
'Job search strategy',
'LinkedIn profile optimisation',
],
accent: 'text-emerald',
glowColor: 'green' as const,
},
{
id: 'self-assessment',
image: '/courses/tax.jpeg',
badge: '02',
title: 'Self Assessment Tax Return',
subtitle: 'File your own return — correctly and on time',
price: '£99',
level: 'Beginner',
duration: '2 hours',
isNew: false,
desc: 'A focused two-hour course walking you through the HMRC Self Assessment process from start to finish. Perfect for sole traders, freelancers, and landlords who want to take control of their tax affairs.',
includes: [
'HMRC registration walkthrough',
'Allowable expenses explained',
'Step-by-step filing guide',
'Deadlines & penalty awareness',
'Common mistakes to avoid',
'Real-world worked examples',
],
accent: 'text-blue',
glowColor: 'blue' as const,
},
{
id: 'payroll',
image: '/courses/payroll.jpeg',
badge: '03',
title: 'Payroll',
subtitle: 'Salaries, taxes & pensions',
price: '£149',
level: 'Intermediate',
duration: 'Self-paced',
isNew: false,
desc: 'A thorough, practical course covering the full UK payroll cycle. Learn to calculate gross and net pay, manage tax codes, run RTI submissions, and stay HMRC-compliant — avoiding costly errors.',
includes: [
'PAYE & tax codes',
'National Insurance calculations',
'Pension auto-enrolment',
'Statutory pay (SSP, SMP, SPP)',
'RTI submissions to HMRC',
'Payslip preparation',
],
accent: 'text-emerald',
glowColor: 'green' as const,
},
{
id: 'quickbooks',
image: '/courses/quickbooks.jpeg',
badge: '04',
title: 'QuickBooks',
subtitle: 'Modern accounting software',
price: '£199',
level: 'BeginnerIntermediate',
duration: 'Self-paced',
isNew: false,
desc: "Master one of the world's most widely used accounting platforms. Learn setup, invoicing, bank reconciliation, and MTD-compliant VAT returns — with practical exercises throughout.",
includes: [
'Setup & chart of accounts',
'Invoicing & expense tracking',
'Bank reconciliation',
'VAT returns & MTD compliance',
'Reporting & dashboards',
'Practical exercises & templates',
],
accent: 'text-blue',
glowColor: 'blue' as const,
},
{
id: 'xero',
image: '/courses/xero.jpeg',
badge: '05',
title: 'Xero',
subtitle: 'Business accounting from scratch',
price: '£199',
level: 'Beginner',
duration: 'New cohort — 2026',
isNew: true,
desc: "The complete beginner's guide to Xero — the UK's most popular cloud accounting platform. Ideal for business owners and anyone building a career in modern bookkeeping.",
includes: [
'Xero setup & navigation',
'Invoices, bills & expenses',
'Bank feeds & reconciliation',
'Payroll basics in Xero',
'VAT returns & MTD compliance',
'Financial reports & insights',
],
accent: 'text-emerald',
glowColor: 'green' as const,
},
];
const WHY_ITEMS = [
{
number: '01',
title: 'Taught by practising accountants',
body: 'Every course is designed and delivered by qualified UK accountants with real-world client experience — not generic online trainers.',
},
{
number: '02',
title: 'One-off payment, lifetime access',
body: 'Pay once and revisit the material whenever you need it. No subscriptions, no renewals, no hidden fees.',
},
{
number: '03',
title: 'Practical, not theoretical',
body: 'Real HMRC scenarios, real software walkthroughs, and real examples — so you can apply what you learn from day one.',
},
];
export default function CoursesPage() {
return (
<>
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,var(--color-emerald-mist)_0%,transparent_70%)]" />
<div className="relative mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<div className="mx-auto max-w-3xl text-center">
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
Professional Development
</p>
<h1 className="font-display text-charcoal mb-6 text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
Accounting courses
<br className="hidden sm:block" /> built for the UK
</h1>
<p className="text-muted mb-8 text-xl leading-relaxed">
Practical, affordable courses taught by qualified accountants. Whether you&apos;re
filing your own tax return or building a career in finance we&apos;ve got a
course for you.
</p>
<div className="flex flex-wrap justify-center gap-3">
<BeamButton size="lg" href="#courses">
Browse Courses
</BeamButton>
<Button size="lg" variant="secondary" href="/contact">
Speak to us first
</Button>
</div>
</div>
</FadeIn>
</div>
</section>
{/* Courses grid */}
<section id="courses" className="bg-white py-24">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{COURSES.map((course, i) => (
<FadeIn
key={course.id}
delay={i * 0.08}
className={course.id === 'xero' ? 'sm:col-span-2 lg:col-span-1' : ''}
>
<SpotlightCard
className="flex h-full flex-col overflow-hidden p-0"
glowColor={course.glowColor}
customSize
>
{/* Thumbnail */}
<div className="relative aspect-video w-full overflow-hidden">
<Image
src={course.image}
alt={course.title}
fill
className="object-cover transition-transform duration-500 hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
{/* Price badge */}
<div className="absolute top-3 right-3">
<span className="font-display rounded-pill text-charcoal bg-white/95 px-3 py-1 text-base font-bold shadow-sm backdrop-blur-sm">
{course.price}
</span>
</div>
{/* New cohort badge */}
{course.isNew && (
<div className="absolute top-3 left-3">
<span className="bg-emerald rounded-pill px-3 py-1 text-xs font-semibold text-white shadow-sm">
New Cohort
</span>
</div>
)}
</div>
{/* Content */}
<div className="flex flex-1 flex-col p-6">
{/* Meta row */}
<div className="text-muted mb-2 flex items-center gap-1.5 text-xs">
<span className="font-mono font-bold">{course.badge}</span>
<span>·</span>
<span>{course.level}</span>
<span>·</span>
<span>{course.duration}</span>
</div>
<h2 className="font-display text-charcoal mb-1 text-xl font-bold">
{course.title}
</h2>
<p className={`mb-3 text-sm font-semibold ${course.accent}`}>
{course.subtitle}
</p>
<p className="text-muted mb-5 text-sm leading-relaxed">{course.desc}</p>
<ul className="mb-5 flex-1 space-y-2">
{course.includes.map((item) => (
<li key={item} className="flex items-center gap-2.5">
<CheckCircleIcon size={14} color="var(--emerald)" />
<span className="text-charcoal text-sm">{item}</span>
</li>
))}
</ul>
<div className="mt-auto border-t border-black/7 pt-4">
<Button
size="sm"
trailingArrow
href="/contact"
className="w-full justify-center"
>
Enrol now
</Button>
</div>
</div>
</SpotlightCard>
</FadeIn>
))}
</div>
</div>
</section>
{/* Why section */}
<section className="bg-bg py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn className="mb-14 flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-end">
<div>
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
Why us
</p>
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">
Learning that
<br />
actually works
</h2>
</div>
</FadeIn>
<div className="grid grid-cols-1 gap-0 border border-black/7 sm:grid-cols-3">
{WHY_ITEMS.map((item, i) => (
<FadeIn key={item.number} delay={i * 0.1} className="h-full">
<div
className={`relative flex h-full flex-col p-8 lg:p-10 ${
i < WHY_ITEMS.length - 1
? 'border-b border-black/7 sm:border-r sm:border-b-0'
: ''
}`}
>
<div className="from-emerald-dark to-emerald shadow-emerald/20 mb-8 flex size-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-md">
<span className="font-mono text-sm font-bold text-white">{item.number}</span>
</div>
<h3 className="font-display text-charcoal mb-3 text-xl font-bold">
{item.title}
</h3>
<p className="text-muted text-sm leading-relaxed">{item.body}</p>
</div>
</FadeIn>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="bg-charcoal py-20">
<div className="mx-auto max-w-[1440px] px-4 text-center sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<h2 className="font-display mb-4 text-3xl font-bold text-white sm:text-4xl">
Not sure which course is right for you?
</h2>
<p className="mx-auto mb-8 max-w-xl text-lg text-white/70">
Drop us a message and we&apos;ll point you in the right direction no obligation,
no hard sell.
</p>
<Button size="lg" trailingArrow href="/contact">
Get in touch
</Button>
</FadeIn>
</div>
</section>
</main>
<Footer />
</>
);
}

View file

@ -8,7 +8,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { Button } from '@/components/ui/Button';
import { MenuIcon, XIcon, ChevronDownIcon } from '@/components/ui/icons';
import { InteractiveMenu } from '@/components/ui/InteractiveMenu';
import { Home, Briefcase, BookOpen, Info, Mail } from 'lucide-react';
import { Home, Briefcase, BookOpen, Info, Mail, GraduationCap } from 'lucide-react';
import { cn } from '@/lib/utils';
const NAV_ITEMS = [
@ -27,6 +27,7 @@ const NAV_ITEMS = [
{ label: 'VAT Returns', href: '/services/vat-returns', desc: 'MTD-compliant filing' },
],
},
{ label: 'Courses', href: '/courses' },
{ label: 'About', href: '/about' },
{ label: 'Blog', href: '/blog' },
{ label: 'Contact', href: '/contact' },
@ -35,6 +36,7 @@ const NAV_ITEMS = [
const DOCK_ITEMS = [
{ label: 'Home', icon: Home, href: '/' },
{ label: 'Services', icon: Briefcase, href: '/services' },
{ label: 'Courses', icon: GraduationCap, href: '/courses' },
{ label: 'Blog', icon: BookOpen, href: '/blog' },
{ label: 'About', icon: Info, href: '/about' },
{ label: 'Contact', icon: Mail, href: '/contact' },
@ -78,11 +80,13 @@ function NavLinks({
onMouseEnter={() => setDropdownOpen(true)}
onMouseLeave={() => setDropdownOpen(false)}
>
<button
<Link
href={item.href}
className={cn(
'rounded-pill text-charcoal hover:bg-emerald-mist flex items-center gap-1 py-2 font-medium transition-colors',
textSize,
px,
pathname === item.href ? 'text-emerald font-semibold' : '',
)}
aria-expanded={dropdownOpen}
aria-haspopup="true"
@ -92,7 +96,7 @@ function NavLinks({
size={12}
className={`transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : ''}`}
/>
</button>
</Link>
{dropdownOpen && (
<div className="rounded-hero border-charcoal/10 absolute top-full left-0 mt-2 w-60 border bg-white p-2 shadow-xl">
{item.dropdown.map((child) => (
@ -182,7 +186,7 @@ export function Header() {
</nav>
<div className="hidden items-center gap-3 lg:flex">
<Button size="sm" trailingArrow>
<Button size="sm" trailingArrow href="/contact">
Book Free Consultation
</Button>
</div>
@ -230,7 +234,7 @@ export function Header() {
</nav>
{/* CTA */}
<Button size="sm" trailingArrow className="ml-1 flex-shrink-0">
<Button size="sm" trailingArrow href="/contact" className="ml-1 flex-shrink-0">
Book Free Consultation
</Button>
</div>
@ -254,7 +258,7 @@ export function Header() {
))}
</nav>
<div className="px-4">
<Button size="lg" className="w-full justify-center" trailingArrow>
<Button size="lg" className="w-full justify-center" trailingArrow href="/contact">
Book Free Consultation
</Button>
</div>

View file

@ -36,12 +36,13 @@ export function FinalCTASection() {
</p>
<div className="mb-10 flex flex-wrap items-center justify-center gap-4">
<BeamButton size="lg" variant="light" trailingArrow>
<BeamButton size="lg" variant="light" trailingArrow href="/contact">
Book a Free Consultation
</BeamButton>
<Button
size="lg"
variant="secondary"
href="/services"
className="border-white/20 text-white hover:border-white/40 hover:bg-white/8"
>
See Our Services

View file

@ -163,10 +163,10 @@ export function HeroSection() {
</p>
<div className="mb-7 flex flex-wrap gap-3">
<BeamButton size="lg" trailingArrow>
<BeamButton size="lg" trailingArrow href="/contact">
Book a Free Consultation
</BeamButton>
<Button size="lg" variant="secondary">
<Button size="lg" variant="secondary" href="/services">
See Our Services
</Button>
</div>

View file

@ -34,7 +34,9 @@ export function HowItWorksSection() {
takes 3 steps
</h2>
</div>
<Button trailingArrow>Book a Free Consultation</Button>
<Button trailingArrow href="/contact">
Book a Free Consultation
</Button>
</FadeIn>
<div className="grid grid-cols-1 gap-0 border border-black/7 sm:grid-cols-3">

View file

@ -1,6 +1,7 @@
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { ArrowRightIcon } from './icons';
import { Spinner } from './Spinner';
@ -14,6 +15,7 @@ interface BeamButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
loading?: boolean;
trailingArrow?: boolean;
leadingIcon?: React.ReactNode;
href?: string;
}
const HEIGHTS: Record<Size, number> = { sm: 36, md: 44, lg: 56 };
@ -46,6 +48,7 @@ export function BeamButton({
leadingIcon,
className = '',
disabled,
href,
...props
}: BeamButtonProps) {
const uid = useId().replace(/:/g, '');
@ -67,70 +70,65 @@ export function BeamButton({
const { bg, text, border } = VARIANTS[variant];
const arrowSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16;
return (
<button
ref={ref}
disabled={disabled || loading}
style={{ height: h }}
className={[
'rounded-pill relative inline-flex cursor-pointer items-center justify-center font-medium',
'transition-all duration-200 select-none',
'active:scale-[0.98]',
'focus-visible:ring-emerald/50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
'disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100',
bg,
text,
PADDINGS[size],
GAPS[size],
className,
].join(' ')}
{...props}
>
{/* Beam border */}
<svg aria-hidden className="pointer-events-none absolute inset-0" width={w} height={h}>
{/* Static base border */}
<rect
x="1"
y="1"
width={w - 2}
height={h - 2}
rx={9999}
fill="none"
stroke={border}
strokeWidth="1.5"
/>
{/* Traveling beam */}
<motion.rect
x="1"
y="1"
width={w - 2}
height={h - 2}
rx={9999}
fill="none"
stroke={`url(#${uid})`}
strokeWidth="2.5"
strokeLinecap="round"
strokeDasharray={`${beamLen} ${perimeter}`}
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: -(perimeter + beamLen) }}
transition={{
duration: 2.8,
repeat: Infinity,
ease: 'linear',
repeatDelay: 0.6,
}}
/>
<defs>
<linearGradient id={uid} gradientUnits="userSpaceOnUse" x1="0" y1="0" x2={w} y2="0">
<stop offset="0%" stopColor="#3CC68A" stopOpacity="0" />
<stop offset="30%" stopColor="#3CC68A" stopOpacity="1" />
<stop offset="70%" stopColor="#1B9AD6" stopOpacity="1" />
<stop offset="100%" stopColor="#1B9AD6" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
const classes = [
'rounded-pill relative inline-flex cursor-pointer items-center justify-center font-medium',
'transition-all duration-200 select-none',
'active:scale-[0.98]',
'focus-visible:ring-emerald/50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
'disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100',
bg,
text,
PADDINGS[size],
GAPS[size],
className,
].join(' ');
{/* Content */}
const beamSvg = (
<svg aria-hidden className="pointer-events-none absolute inset-0" width={w} height={h}>
<rect
x="1"
y="1"
width={w - 2}
height={h - 2}
rx={9999}
fill="none"
stroke={border}
strokeWidth="1.5"
/>
<motion.rect
x="1"
y="1"
width={w - 2}
height={h - 2}
rx={9999}
fill="none"
stroke={`url(#${uid})`}
strokeWidth="2.5"
strokeLinecap="round"
strokeDasharray={`${beamLen} ${perimeter}`}
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: -(perimeter + beamLen) }}
transition={{
duration: 2.8,
repeat: Infinity,
ease: 'linear',
repeatDelay: 0.6,
}}
/>
<defs>
<linearGradient id={uid} gradientUnits="userSpaceOnUse" x1="0" y1="0" x2={w} y2="0">
<stop offset="0%" stopColor="#3CC68A" stopOpacity="0" />
<stop offset="30%" stopColor="#3CC68A" stopOpacity="1" />
<stop offset="70%" stopColor="#1B9AD6" stopOpacity="1" />
<stop offset="100%" stopColor="#1B9AD6" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
const content = (
<>
{beamSvg}
{loading ? <Spinner size={size === 'lg' ? 'md' : 'sm'} /> : (leadingIcon ?? null)}
{children}
{!loading && trailingArrow && (
@ -139,6 +137,26 @@ export function BeamButton({
className="shrink-0 transition-transform duration-200 group-hover:translate-x-0.5"
/>
)}
</>
);
if (href) {
return (
<Link href={href} style={{ height: h }} className={classes}>
{content}
</Link>
);
}
return (
<button
ref={ref}
disabled={disabled || loading}
style={{ height: h }}
className={classes}
{...props}
>
{content}
</button>
);
}

View file

@ -1,6 +1,7 @@
'use client';
import { forwardRef } from 'react';
import Link from 'next/link';
import { Spinner } from './Spinner';
import { ArrowRightIcon } from './icons';
@ -13,6 +14,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
trailingArrow?: boolean;
leadingIcon?: React.ReactNode;
href?: string;
}
const variants: Record<Variant, string> = {
@ -42,28 +44,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
children,
className = '',
disabled,
href,
...props
},
ref,
) => {
const arrowSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16;
return (
<button
ref={ref}
disabled={disabled || loading}
className={[
'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(' ')}
{...props}
>
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 ? (
@ -76,6 +76,20 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
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>
);
},