@majordigital/create-acorn 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +35 -70
  2. package/bin/create-acorn.mjs +28 -2
  3. package/package.json +2 -1
  4. package/template/next.config.js +48 -0
  5. package/template/public/favicon/favicon.ico +0 -0
  6. package/template/src/app/layout.tsx +60 -0
  7. package/template/src/app/not-found.tsx +7 -0
  8. package/template/src/app/page.tsx +7 -0
  9. package/template/src/app/robots.ts +32 -0
  10. package/template/src/app/sitemap.ts +22 -0
  11. package/template/src/icons/logo.svg +3 -0
  12. package/template/src/lib/buildCache.ts +29 -0
  13. package/template/src/lib/config.ts +7 -0
  14. package/template/src/lib/constants.ts +0 -0
  15. package/template/src/lib/fonts.ts +15 -0
  16. package/template/src/lib/getMetadata.ts +124 -0
  17. package/template/src/lib/utils.ts +12 -0
  18. package/template/src/styles/globals.css +23 -0
  19. package/template/src/types/components.ts +25 -0
  20. package/template/src/types/custom.d.ts +3 -0
  21. package/template/src/ui/ConditionalWrapper.tsx +13 -0
  22. package/template/src/ui/components/Accordion.tsx +73 -0
  23. package/template/src/ui/components/AnnouncementBar.tsx +36 -0
  24. package/template/src/ui/components/Breadcrumbs.tsx +60 -0
  25. package/template/src/ui/components/ButtonGroup.tsx +42 -0
  26. package/template/src/ui/components/CallToAction.tsx +21 -0
  27. package/template/src/ui/components/Card.tsx +21 -0
  28. package/template/src/ui/components/FeaturedContent.tsx +21 -0
  29. package/template/src/ui/components/FormContact.tsx +190 -0
  30. package/template/src/ui/components/Nav.tsx +39 -0
  31. package/template/src/ui/components/NavCollapsed.tsx +91 -0
  32. package/template/src/ui/components/Pagination.tsx +96 -0
  33. package/template/src/ui/components/Quote.tsx +21 -0
  34. package/template/src/ui/elements/Button.tsx +97 -0
  35. package/template/src/ui/elements/ButtonWrapper.tsx +42 -0
  36. package/template/src/ui/elements/Chip.tsx +27 -0
  37. package/template/src/ui/elements/Tooltip.tsx +71 -0
  38. package/template/src/ui/elements/form/Checkbox.tsx +24 -0
  39. package/template/src/ui/elements/form/Form.tsx +134 -0
  40. package/template/src/ui/elements/form/FormLabel.tsx +15 -0
  41. package/template/src/ui/elements/form/FormMessage.tsx +34 -0
  42. package/template/src/ui/elements/form/Input.tsx +24 -0
  43. package/template/src/ui/elements/form/Textarea.tsx +24 -0
  44. package/template/src/ui/elements/navigation/NavPopover.tsx +84 -0
  45. package/template/src/ui/elements/navigation/NavPrimaryLink.tsx +27 -0
  46. package/template/src/ui/elements/navigation/NavSecondaryLink.tsx +28 -0
  47. package/template/src/ui/elements/typography/Blockquote.tsx +30 -0
  48. package/template/src/ui/elements/typography/H.tsx +92 -0
  49. package/template/src/ui/elements/typography/List.tsx +64 -0
  50. package/template/src/ui/elements/typography/P.tsx +88 -0
  51. package/template/src/ui/elements/typography/TypoWrapper.tsx +15 -0
  52. package/template/src/ui/layout/Container.tsx +27 -0
  53. package/template/src/ui/layout/PageSection.tsx +39 -0
  54. package/template/src/ui/sections/Footer.tsx +11 -0
  55. package/template/src/ui/sections/Header.tsx +21 -0
  56. package/template/tailwind.config.js +33 -0
  57. package/template/tsconfig.json +69 -0
@@ -0,0 +1,73 @@
1
+ import { MinusIcon, PlusIcon } from '@heroicons/react/24/solid';
2
+ import clsx from 'clsx';
3
+ import {
4
+ Accordion as AccordionPanel,
5
+ AccordionItem,
6
+ AccordionItemButton,
7
+ AccordionItemHeading,
8
+ AccordionItemPanel,
9
+ AccordionItemState,
10
+ } from 'react-accessible-accordion';
11
+
12
+ interface AccordionProps {
13
+ children: AccordionItemProps[];
14
+ className?: string;
15
+ }
16
+
17
+ const Accordion = ({ children, className }: AccordionProps) => {
18
+ if (!children) return null;
19
+
20
+ return (
21
+ <AccordionPanel
22
+ allowMultipleExpanded
23
+ allowZeroExpanded
24
+ className={clsx('w-full', className)}
25
+ >
26
+ {children.map(panel => (
27
+ <Accordion.Item
28
+ key={panel.id}
29
+ id={panel.id}
30
+ label={panel.label}
31
+ >
32
+ {panel.children}
33
+ </Accordion.Item>
34
+ ))}
35
+ </AccordionPanel>
36
+ );
37
+ };
38
+
39
+ interface AccordionItemProps {
40
+ id: string;
41
+ label: string;
42
+ children: React.ReactNode;
43
+ }
44
+
45
+ const Item = ({ id, label, children }: AccordionItemProps) => {
46
+ return (
47
+ <AccordionItem uuid={id} className="mb-6 border-b">
48
+ <AccordionItemHeading className="">
49
+ <AccordionItemButton className="flex place-content-between items-center gap-6 pb-6">
50
+ {label}
51
+ <AccordionItemState>
52
+ {({ expanded }) =>
53
+ !expanded ? (
54
+ <PlusIcon
55
+ className="size-10 shrink-0"
56
+ aria-label="Open panel"
57
+ />
58
+ ) : (
59
+ <MinusIcon
60
+ className="size-10 shrink-0"
61
+ aria-label="Close panel"
62
+ />
63
+ )
64
+ }
65
+ </AccordionItemState>
66
+ </AccordionItemButton>
67
+ </AccordionItemHeading>
68
+ <AccordionItemPanel className="pb-6">{children}</AccordionItemPanel>
69
+ </AccordionItem>
70
+ );
71
+ };
72
+
73
+ Accordion.Item = Item;
@@ -0,0 +1,36 @@
1
+ import { XMarkIcon } from '@heroicons/react/24/solid';
2
+ import clsx from 'clsx';
3
+
4
+ interface AnnouncementBarProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ const AnnouncementBar = ({ children, className }: AnnouncementBarProps) => {
10
+ return (
11
+ <div
12
+ className={clsx(
13
+ 'relative isolate flex items-center gap-x-6 overflow-hidden bg-gray-50 px-6 py-2.5 sm:px-3.5 sm:before:flex-1',
14
+ className
15
+ )}
16
+ >
17
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
18
+ {children}
19
+ </div>
20
+ <div className="flex flex-1 justify-end">
21
+ <button
22
+ type="button"
23
+ className="-m-3 p-3 focus-visible:outline-offset-[-4px]"
24
+ >
25
+ <span className="sr-only">Dismiss</span>
26
+ <XMarkIcon
27
+ className="size-5 text-gray-900"
28
+ aria-hidden="true"
29
+ />
30
+ </button>
31
+ </div>
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export default AnnouncementBar;
@@ -0,0 +1,60 @@
1
+ import clsx from 'clsx';
2
+ import Link from 'next/link';
3
+ import type { ComponentProps } from 'react';
4
+
5
+ interface BreadcrumbProps extends ComponentProps<'nav'> {
6
+ breadcrumbs: BreadcrumbItemProps[];
7
+ className?: string;
8
+ }
9
+ interface BreadcrumbItemProps {
10
+ label: string;
11
+ href: string;
12
+ isActive?: boolean;
13
+ }
14
+
15
+ const Breadcrumbs = ({ breadcrumbs, className, ...props }: BreadcrumbProps) => {
16
+ const itemClassName = 'flex gap-2 py-2';
17
+ const textClassName =
18
+ 'text-xs lg:text-sm capitalize font-light hover:underline';
19
+
20
+ return (
21
+ <nav
22
+ aria-label="Breadcrumbs"
23
+ className={clsx('mb-4 block', className)}
24
+ {...props}
25
+ >
26
+ <ol className="flex gap-2">
27
+ <li className={itemClassName}>
28
+ <Link href="/" className={textClassName}>
29
+ Home
30
+ </Link>
31
+ <span className={textClassName}>/</span>
32
+ </li>
33
+ {breadcrumbs.map((crumb: BreadcrumbItemProps, key: number) => (
34
+ <li
35
+ aria-current={crumb.isActive}
36
+ className={clsx(itemClassName, {
37
+ '': crumb.isActive,
38
+ })}
39
+ key={key}
40
+ >
41
+ <Link href={crumb.href} className={textClassName}>
42
+ {crumb.label}
43
+ </Link>
44
+ {key + 1 !== breadcrumbs.length && (
45
+ <span
46
+ className={textClassName}
47
+ aria-disabled="true"
48
+ aria-current="page"
49
+ >
50
+ /
51
+ </span>
52
+ )}
53
+ </li>
54
+ ))}
55
+ </ol>
56
+ </nav>
57
+ );
58
+ };
59
+
60
+ export default Breadcrumbs;
@@ -0,0 +1,42 @@
1
+ import clsx from 'clsx';
2
+ import type { HTMLAttributes } from 'react';
3
+ import React from 'react';
4
+
5
+ interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
6
+ as?: React.ElementType;
7
+ align?: 'left' | 'center' | 'right' | 'stretch';
8
+ spacing?: 'md' | 'lg' | 'xl' | 'auto';
9
+ }
10
+
11
+ const ButtonGroup = ({
12
+ children,
13
+ as: Component = 'div',
14
+ className,
15
+ align = 'left',
16
+ spacing,
17
+ ...props
18
+ }: ButtonGroupProps) => {
19
+ const alignClass = clsx('flex flex-col items-center sm:flex-row', {
20
+ 'justify-start': align === 'left',
21
+ 'justify-center': align === 'center',
22
+ 'justify-end': align === 'right',
23
+ 'justify-stretch': align === 'stretch',
24
+ });
25
+
26
+ const spacingClass = clsx({
27
+ 'gap-4': spacing === 'md',
28
+ 'gap-8': spacing === 'lg',
29
+ 'gap-12': spacing === 'xl',
30
+ });
31
+
32
+ return (
33
+ <Component
34
+ className={clsx(alignClass, spacingClass, className)}
35
+ {...props}
36
+ >
37
+ {children}
38
+ </Component>
39
+ );
40
+ };
41
+
42
+ export default ButtonGroup;
@@ -0,0 +1,21 @@
1
+ import clsx from 'clsx';
2
+ import type { HTMLAttributes } from 'react';
3
+
4
+ interface CallToActionProps extends HTMLAttributes<HTMLDivElement> {
5
+ as?: React.ElementType;
6
+ }
7
+
8
+ const CallToAction = ({
9
+ children,
10
+ as: Component = 'section',
11
+ className,
12
+ ...props
13
+ }: CallToActionProps) => {
14
+ return (
15
+ <Component className={clsx('rounded', className)} {...props}>
16
+ {children}
17
+ </Component>
18
+ );
19
+ };
20
+
21
+ export default CallToAction;
@@ -0,0 +1,21 @@
1
+ import clsx from 'clsx';
2
+ import type { HTMLAttributes } from 'react';
3
+
4
+ interface CardProps extends HTMLAttributes<HTMLDivElement> {
5
+ as?: React.ElementType;
6
+ }
7
+
8
+ const Card = ({
9
+ children,
10
+ as: Component = 'article',
11
+ className,
12
+ ...props
13
+ }: CardProps) => {
14
+ return (
15
+ <Component className={clsx('', className)} {...props}>
16
+ {children}
17
+ </Component>
18
+ );
19
+ };
20
+
21
+ export default Card;
@@ -0,0 +1,21 @@
1
+ import clsx from 'clsx';
2
+ import type { HTMLAttributes } from 'react';
3
+
4
+ interface FeaturedContentProps extends HTMLAttributes<HTMLDivElement> {
5
+ as?: React.ElementType;
6
+ }
7
+
8
+ const FeaturedContent = ({
9
+ children,
10
+ as: Component = 'div',
11
+ className,
12
+ ...props
13
+ }: FeaturedContentProps) => {
14
+ return (
15
+ <Component className={clsx('', className)} {...props}>
16
+ {children}
17
+ </Component>
18
+ );
19
+ };
20
+
21
+ export default FeaturedContent;
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import clsx from 'clsx';
5
+ import React, { useRef, useState } from 'react';
6
+ import { useForm } from 'react-hook-form';
7
+ import { z } from 'zod';
8
+
9
+ import Button from '@/ui/elements/Button';
10
+ import H from '@/ui/elements/typography/H';
11
+ import P from '@/ui/elements/typography/P';
12
+
13
+ import { Form, FormControl, FormField, FormItem } from '../elements/form/Form';
14
+ import FormLabel from '../elements/form/FormLabel';
15
+ import FormMessage from '../elements/form/FormMessage';
16
+ import Input from '../elements/form/Input';
17
+ import Textarea from '../elements/form/Textarea';
18
+
19
+ const FormSchema = z.object({
20
+ firstname: z.string().min(2, {
21
+ message: 'First name must be at least 2 characters.',
22
+ }),
23
+ lastname: z.string().min(2, {
24
+ message: 'Last name must be at least 2 characters.',
25
+ }),
26
+ email: z
27
+ .string()
28
+ .min(2, {
29
+ message: 'Email must be at least 2 characters.',
30
+ })
31
+ .email('Please enter a valid valid email.'),
32
+ phone: z.string().min(2, {
33
+ message: 'Phone must be at least 2 characters.',
34
+ }),
35
+ message: z.string().min(2, {
36
+ message: 'Message must be at least 2 characters.',
37
+ }),
38
+ });
39
+
40
+ const FormContact = ({ className }: { className?: string }) => {
41
+ const form = useForm<z.infer<typeof FormSchema>>({
42
+ resolver: zodResolver(FormSchema),
43
+ defaultValues: {
44
+ firstname: '',
45
+ lastname: '',
46
+ email: '',
47
+ phone: '',
48
+ message: '',
49
+ },
50
+ });
51
+
52
+ const [status, setStatus] = useState('');
53
+ const [error, setError] = useState('');
54
+
55
+ const formRef = useRef<null | HTMLFormElement>(null);
56
+
57
+ const onSubmit = async (data: z.infer<typeof FormSchema>) => {
58
+ try {
59
+ const res = await fetch('/', {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/x-www-form-urlencoded',
63
+ },
64
+ body: new URLSearchParams({
65
+ 'form-name': 'contact',
66
+ ...data,
67
+ }).toString(),
68
+ });
69
+ if (res.status === 200) {
70
+ setStatus('sent');
71
+ formRef?.current?.scrollIntoView({
72
+ behavior: 'smooth',
73
+ block: 'end',
74
+ });
75
+ } else {
76
+ setStatus('error');
77
+ setError(`${res.status} ${res.statusText}`);
78
+ }
79
+ } catch (e) {
80
+ setStatus('error');
81
+ setError(`${e}`);
82
+ }
83
+ };
84
+
85
+ return status === 'sent' ? (
86
+ <div className={clsx('m-auto w-96 max-w-full text-center', className)}>
87
+ <H level={3} spacing="b">
88
+ Success!
89
+ </H>
90
+ <P>
91
+ Thank you for your enquiry. One of our team will be in touch
92
+ shortly.
93
+ </P>
94
+ </div>
95
+ ) : (
96
+ <Form {...form}>
97
+ <form
98
+ ref={formRef}
99
+ id="contact"
100
+ name="contact"
101
+ data-netlify="true"
102
+ data-netlify-honeypot="bot-field"
103
+ onSubmit={form.handleSubmit(onSubmit)}
104
+ className={clsx('grid max-w-xl grid-cols-2 gap-6', className)}
105
+ >
106
+ <label hidden>
107
+ Don&apos;t fill this out if you&apos;re human:{' '}
108
+ <input name="bot-field" />
109
+ </label>
110
+ <input type="hidden" name="form-name" value="contact" />
111
+ <FormField
112
+ control={form.control}
113
+ name="firstname"
114
+ render={({ field }) => (
115
+ <FormItem className="col-span-1">
116
+ <FormLabel>First name</FormLabel>
117
+ <FormControl>
118
+ <Input {...field} />
119
+ </FormControl>
120
+ <FormMessage />
121
+ </FormItem>
122
+ )}
123
+ />
124
+ <FormField
125
+ control={form.control}
126
+ name="lastname"
127
+ render={({ field }) => (
128
+ <FormItem className="col-span-1">
129
+ <FormLabel>Last name</FormLabel>
130
+ <FormControl>
131
+ <Input {...field} />
132
+ </FormControl>
133
+ <FormMessage />
134
+ </FormItem>
135
+ )}
136
+ />
137
+ <FormField
138
+ control={form.control}
139
+ name="email"
140
+ render={({ field }) => (
141
+ <FormItem>
142
+ <FormLabel>Email address</FormLabel>
143
+ <FormControl>
144
+ <Input {...field} />
145
+ </FormControl>
146
+ <FormMessage />
147
+ </FormItem>
148
+ )}
149
+ />
150
+ <FormField
151
+ control={form.control}
152
+ name="phone"
153
+ render={({ field }) => (
154
+ <FormItem>
155
+ <FormLabel>Phone</FormLabel>
156
+ <FormControl>
157
+ <Input {...field} />
158
+ </FormControl>
159
+ <FormMessage />
160
+ </FormItem>
161
+ )}
162
+ />
163
+ <FormField
164
+ control={form.control}
165
+ name="message"
166
+ render={({ field }) => (
167
+ <FormItem>
168
+ <FormLabel>Message</FormLabel>
169
+ <FormControl>
170
+ <Textarea {...field} />
171
+ </FormControl>
172
+ <FormMessage />
173
+ </FormItem>
174
+ )}
175
+ />
176
+ {status === 'error' && (
177
+ <FormMessage>Error: {error}</FormMessage>
178
+ )}
179
+
180
+ <div className="col-span-full">
181
+ <Button type="submit" disabled={status === 'pending'}>
182
+ Submit form
183
+ </Button>
184
+ </div>
185
+ </form>
186
+ </Form>
187
+ );
188
+ };
189
+
190
+ export default FormContact;
@@ -0,0 +1,39 @@
1
+ import clsx from 'clsx';
2
+ import type { ComponentProps } from 'react';
3
+
4
+ import NavPrimaryLink from '@/ui/elements/navigation/NavPrimaryLink';
5
+
6
+ import {
7
+ NavPopover,
8
+ NavPopoverButton,
9
+ NavPopoverPanel,
10
+ } from '../elements/navigation/NavPopover';
11
+
12
+ const NavItem = ({ children, className, ...props }: ComponentProps<'li'>) => (
13
+ <li className={clsx('block', className)} {...props}>
14
+ {children}
15
+ </li>
16
+ );
17
+
18
+ const Nav = ({ ...props }: ComponentProps<'nav'>) => (
19
+ <nav className="hidden shrink-0 lg:block" {...props}>
20
+ <ul className="flex gap-4 xl:gap-8">
21
+ <NavItem>
22
+ <NavPrimaryLink href="/">Home</NavPrimaryLink>
23
+ </NavItem>
24
+ <NavItem>
25
+ <NavPopover>
26
+ <NavPopoverButton href="/test">
27
+ Popover Button
28
+ </NavPopoverButton>
29
+ <NavPopoverPanel>Panel contents</NavPopoverPanel>
30
+ </NavPopover>
31
+ </NavItem>
32
+ <NavItem>
33
+ <NavPrimaryLink href="/">Home</NavPrimaryLink>
34
+ </NavItem>
35
+ </ul>
36
+ </nav>
37
+ );
38
+
39
+ export default Nav;
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Dialog,
5
+ DialogPanel,
6
+ Transition,
7
+ TransitionChild,
8
+ } from '@headlessui/react';
9
+ import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/solid';
10
+ import { Fragment, useState } from 'react';
11
+
12
+ import NavPrimaryLink from '@/ui/elements/navigation/NavPrimaryLink';
13
+ import Container from '@/ui/layout/Container';
14
+
15
+ const NavItem = ({ title, href }: { title: string; href: string }) => (
16
+ <li className="block">
17
+ <NavPrimaryLink href={href}>{title}</NavPrimaryLink>
18
+ </li>
19
+ );
20
+
21
+ const NavCollapsed = () => {
22
+ const [isOpen, setIsOpen] = useState(false);
23
+
24
+ return (
25
+ <>
26
+ <button
27
+ id="menubutton"
28
+ aria-label="Open menu"
29
+ aria-haspopup="true"
30
+ onClick={() => setIsOpen(true)}
31
+ className="grow-0 lg:hidden"
32
+ >
33
+ <Bars3Icon className="h-10" />
34
+ </button>
35
+ <Transition show={isOpen} as={Fragment}>
36
+ <Dialog
37
+ id="menu"
38
+ onClose={() => setIsOpen(false)}
39
+ as="nav"
40
+ aria-labelledby="menubutton"
41
+ >
42
+ <div className="fixed inset-0 z-50 overflow-hidden">
43
+ <div className="absolute inset-0 overflow-hidden">
44
+ <div className="fixed inset-y-0 right-0 flex">
45
+ <TransitionChild
46
+ as={Fragment}
47
+ enter="transform transition ease-in-out duration-500 sm:duration-700"
48
+ enterFrom="translate-x-full"
49
+ enterTo="translate-x-0"
50
+ leave="transform transition ease-in-out duration-500 sm:duration-700"
51
+ leaveFrom="translate-x-0"
52
+ leaveTo="translate-x-full"
53
+ >
54
+ <DialogPanel
55
+ className={`bg-blue-500 bg-gradient-to-t from-blue-500 font-sans lg:hidden`}
56
+ >
57
+ <Container className="flex items-center justify-end overflow-hidden py-2">
58
+ <button
59
+ id="menubutton"
60
+ aria-label="Close menu"
61
+ aria-haspopup="true"
62
+ onClick={() => setIsOpen(false)}
63
+ className="flex items-center gap-4 text-sm uppercase text-white"
64
+ >
65
+ <span>Close</span>
66
+ <XMarkIcon className="h-12 fill-white" />
67
+ </button>
68
+ </Container>
69
+ <nav
70
+ className="shrink-0 px-4 text-white lg:hidden"
71
+ aria-label="Main"
72
+ >
73
+ <ul className="flex flex-col">
74
+ <NavItem
75
+ title="Home"
76
+ href="/"
77
+ />
78
+ </ul>
79
+ </nav>
80
+ </DialogPanel>
81
+ </TransitionChild>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </Dialog>
86
+ </Transition>
87
+ </>
88
+ );
89
+ };
90
+
91
+ export default NavCollapsed;
@@ -0,0 +1,96 @@
1
+ import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
2
+ import clsx from 'clsx';
3
+ import Link from 'next/link';
4
+ import type { AnchorHTMLAttributes, ComponentProps } from 'react';
5
+
6
+ const Pagination = ({
7
+ children,
8
+ className,
9
+ ...props
10
+ }: ComponentProps<'nav'>) => {
11
+ return (
12
+ <nav
13
+ role="navigation"
14
+ aria-label="pagination"
15
+ className={clsx('mx-auto flex w-full justify-center', className)}
16
+ {...props}
17
+ >
18
+ <ul className="flex flex-row items-center gap-1">{children}</ul>
19
+ </nav>
20
+ );
21
+ };
22
+
23
+ const PaginationItem = ({
24
+ children,
25
+ className,
26
+ ...props
27
+ }: ComponentProps<'li'>) => {
28
+ return (
29
+ <li
30
+ className={clsx('mx-auto flex w-full justify-center', className)}
31
+ {...props}
32
+ >
33
+ {children}
34
+ </li>
35
+ );
36
+ };
37
+
38
+ interface PaginationLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
39
+ href: string;
40
+ isActive?: boolean;
41
+ }
42
+
43
+ const PaginationLink = ({
44
+ href,
45
+ children,
46
+ className,
47
+ isActive,
48
+ ...props
49
+ }: PaginationLinkProps) => {
50
+ return (
51
+ <Link
52
+ href={href}
53
+ aria-current={isActive ? 'page' : undefined}
54
+ className={clsx('', className)}
55
+ {...props}
56
+ >
57
+ {children}
58
+ </Link>
59
+ );
60
+ };
61
+
62
+ const PaginationPrev = ({ href, className, ...props }: PaginationLinkProps) => {
63
+ return (
64
+ <Link
65
+ href={href}
66
+ aria-label="Go to previous page"
67
+ className={clsx('', className)}
68
+ {...props}
69
+ >
70
+ <ChevronLeftIcon className="size-4" />
71
+ <span>Previous</span>
72
+ </Link>
73
+ );
74
+ };
75
+
76
+ const PaginationNext = ({ href, className, ...props }: PaginationLinkProps) => {
77
+ return (
78
+ <Link
79
+ href={href}
80
+ aria-label="Go to next page"
81
+ className={clsx('', className)}
82
+ {...props}
83
+ >
84
+ <span>Next</span>
85
+ <ChevronRightIcon className="size-4" />
86
+ </Link>
87
+ );
88
+ };
89
+
90
+ export {
91
+ Pagination,
92
+ PaginationItem,
93
+ PaginationLink,
94
+ PaginationNext,
95
+ PaginationPrev,
96
+ };