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>
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-code-plugins": false,
|
||||
"frontend-design@claude-code-plugins": true,
|
||||
"feature-dev@claude-code-plugins": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
After Width: | Height: | Size: 116 KiB |
BIN
Assets/payroll-1.jpeg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
Assets/quickbooks.jpeg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
Assets/tax.jpeg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
Assets/xero.jpeg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
public/courses/cv.jpeg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
public/courses/payroll.jpeg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/courses/quickbooks.jpeg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
public/courses/tax.jpeg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/courses/xero.jpeg
Normal file
|
After Width: | Height: | Size: 107 KiB |
|
|
@ -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: 'Mon–Fri, 9am–6pm GMT',
|
||||
value: 'Mon–Fri: 9am–5pm · 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
|
|
@ -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: 'Beginner–Intermediate',
|
||||
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're
|
||||
filing your own tax return or building a career in finance — we'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'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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
|||