@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.
- package/README.md +35 -70
- package/bin/create-acorn.mjs +28 -2
- package/package.json +2 -1
- package/template/next.config.js +48 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/src/app/layout.tsx +60 -0
- package/template/src/app/not-found.tsx +7 -0
- package/template/src/app/page.tsx +7 -0
- package/template/src/app/robots.ts +32 -0
- package/template/src/app/sitemap.ts +22 -0
- package/template/src/icons/logo.svg +3 -0
- package/template/src/lib/buildCache.ts +29 -0
- package/template/src/lib/config.ts +7 -0
- package/template/src/lib/constants.ts +0 -0
- package/template/src/lib/fonts.ts +15 -0
- package/template/src/lib/getMetadata.ts +124 -0
- package/template/src/lib/utils.ts +12 -0
- package/template/src/styles/globals.css +23 -0
- package/template/src/types/components.ts +25 -0
- package/template/src/types/custom.d.ts +3 -0
- package/template/src/ui/ConditionalWrapper.tsx +13 -0
- package/template/src/ui/components/Accordion.tsx +73 -0
- package/template/src/ui/components/AnnouncementBar.tsx +36 -0
- package/template/src/ui/components/Breadcrumbs.tsx +60 -0
- package/template/src/ui/components/ButtonGroup.tsx +42 -0
- package/template/src/ui/components/CallToAction.tsx +21 -0
- package/template/src/ui/components/Card.tsx +21 -0
- package/template/src/ui/components/FeaturedContent.tsx +21 -0
- package/template/src/ui/components/FormContact.tsx +190 -0
- package/template/src/ui/components/Nav.tsx +39 -0
- package/template/src/ui/components/NavCollapsed.tsx +91 -0
- package/template/src/ui/components/Pagination.tsx +96 -0
- package/template/src/ui/components/Quote.tsx +21 -0
- package/template/src/ui/elements/Button.tsx +97 -0
- package/template/src/ui/elements/ButtonWrapper.tsx +42 -0
- package/template/src/ui/elements/Chip.tsx +27 -0
- package/template/src/ui/elements/Tooltip.tsx +71 -0
- package/template/src/ui/elements/form/Checkbox.tsx +24 -0
- package/template/src/ui/elements/form/Form.tsx +134 -0
- package/template/src/ui/elements/form/FormLabel.tsx +15 -0
- package/template/src/ui/elements/form/FormMessage.tsx +34 -0
- package/template/src/ui/elements/form/Input.tsx +24 -0
- package/template/src/ui/elements/form/Textarea.tsx +24 -0
- package/template/src/ui/elements/navigation/NavPopover.tsx +84 -0
- package/template/src/ui/elements/navigation/NavPrimaryLink.tsx +27 -0
- package/template/src/ui/elements/navigation/NavSecondaryLink.tsx +28 -0
- package/template/src/ui/elements/typography/Blockquote.tsx +30 -0
- package/template/src/ui/elements/typography/H.tsx +92 -0
- package/template/src/ui/elements/typography/List.tsx +64 -0
- package/template/src/ui/elements/typography/P.tsx +88 -0
- package/template/src/ui/elements/typography/TypoWrapper.tsx +15 -0
- package/template/src/ui/layout/Container.tsx +27 -0
- package/template/src/ui/layout/PageSection.tsx +39 -0
- package/template/src/ui/sections/Footer.tsx +11 -0
- package/template/src/ui/sections/Header.tsx +21 -0
- package/template/tailwind.config.js +33 -0
- 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't fill this out if you'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
|
+
};
|