Skip to content

Commit a6941a8

Browse files
committed
feat(website): add simple captcha
1 parent b0b44ad commit a6941a8

File tree

1 file changed

+158
-115
lines changed

1 file changed

+158
-115
lines changed
Lines changed: 158 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,170 @@
1-
'use client'
2-
import { Portal } from '@ark-ui/react/portal'
3-
import { MessageSquareIcon, XIcon } from 'lucide-react'
4-
import { forwardRef, useActionState, useState } from 'react'
5-
import { HStack, Stack } from 'styled-system/jsx'
6-
import { contact } from '~/app/actions'
7-
import { Button } from '~/components/ui/button'
8-
import { Dialog } from '~/components/ui/dialog'
9-
import { Field } from '~/components/ui/field'
10-
import { useUpdateEffect } from '~/lib/use-update-effect'
11-
import { SubmitButton } from './submit-button'
12-
import { toaster } from './toaster'
13-
import { IconButton, type IconButtonProps } from './ui/icon-button'
14-
import { Input } from './ui/input'
15-
import { Textarea } from './ui/textarea'
1+
"use client";
2+
import { Portal } from "@ark-ui/react/portal";
3+
import { MessageSquareIcon, XIcon } from "lucide-react";
4+
import { forwardRef, useActionState, useEffect, useState } from "react";
5+
import { HStack, Stack } from "styled-system/jsx";
6+
import { contact } from "~/app/actions";
7+
import { Button } from "~/components/ui/button";
8+
import { Dialog } from "~/components/ui/dialog";
9+
import { Field } from "~/components/ui/field";
10+
import { useUpdateEffect } from "~/lib/use-update-effect";
11+
import { SubmitButton } from "./submit-button";
12+
import { toaster } from "./toaster";
13+
import { IconButton, type IconButtonProps } from "./ui/icon-button";
14+
import { Input } from "./ui/input";
15+
import { Textarea } from "./ui/textarea";
1616

1717
interface Props {
18-
subject?: string
19-
children: React.ReactNode
18+
subject?: string;
19+
children: React.ReactNode;
2020
}
2121

2222
export const ContactDialog = (props: Props) => {
23-
const { subject = 'General', children } = props
24-
const [state, formAction] = useActionState(contact, { message: '', success: undefined })
25-
const [open, setOpen] = useState(false)
23+
const { subject = "General", children } = props;
24+
const [state, formAction] = useActionState(contact, {
25+
message: "",
26+
success: undefined,
27+
});
28+
const [open, setOpen] = useState(false);
29+
const [num1, setNum1] = useState(0);
30+
const [num2, setNum2] = useState(0);
2631

27-
useUpdateEffect(() => {
28-
if (state.success) {
29-
setOpen(false)
30-
toaster.create({ title: state.message })
31-
} else {
32-
toaster.error({ title: state.message })
33-
}
34-
}, [state])
32+
useEffect(() => {
33+
if (open) {
34+
setNum1(Math.floor(Math.random() * 10));
35+
setNum2(Math.floor(Math.random() * 10));
36+
}
37+
}, [open]);
3538

36-
return (
37-
<Dialog.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
38-
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
39-
<Portal>
40-
<Dialog.Backdrop />
41-
<Dialog.Positioner>
42-
<Dialog.Content p="4">
43-
<form action={formAction}>
44-
<Stack gap="1">
45-
<Dialog.Title textStyle="lg">Contact Us</Dialog.Title>
46-
<Dialog.Description textStyle="sm">
47-
Questions? We'll get back to you as soon as possible.
48-
</Dialog.Description>
49-
</Stack>
39+
const handleSubmit = async (formData: FormData) => {
40+
const answer = Number.parseInt(formData.get("captcha") as string);
41+
if (answer !== num1 + num2) {
42+
toaster.error({ title: "Incorrect captcha answer" });
43+
return;
44+
}
45+
formAction(formData);
46+
};
5047

51-
<Stack gap={{ base: '6', md: '8' }}>
52-
<Stack gap="4" pt="5">
53-
<Field.Root required>
54-
<Field.Label>Name</Field.Label>
55-
<Field.Input type="text" asChild>
56-
<Input placeholder="John Doe" name="name" required />
57-
</Field.Input>
58-
</Field.Root>
59-
<Field.Root required>
60-
<Field.Label>E-Mail</Field.Label>
61-
<Field.Input asChild>
62-
<Input placeholder="me@example.com" name="email" type="email" required />
63-
</Field.Input>
64-
</Field.Root>
65-
<Field.Root>
66-
<Field.Label>Message</Field.Label>
67-
<Field.Textarea autoresize asChild>
68-
<Textarea
69-
name="message"
70-
placeholder="How can we assist you?"
71-
required
72-
rows={4}
73-
/>
74-
</Field.Textarea>
75-
</Field.Root>
48+
useUpdateEffect(() => {
49+
if (state.success) {
50+
setOpen(false);
51+
toaster.create({ title: state.message });
52+
} else {
53+
toaster.error({ title: state.message });
54+
}
55+
}, [state]);
7656

77-
<input type="hidden" name="subject" value={subject} />
78-
</Stack>
79-
</Stack>
80-
<HStack pt="8" justifyContent="flex-end">
81-
<Button variant="outline" onClick={() => setOpen(false)}>
82-
Cancel
83-
</Button>
84-
<SubmitButton>Submit</SubmitButton>
85-
</HStack>
57+
return (
58+
<Dialog.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
59+
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
60+
<Portal>
61+
<Dialog.Backdrop />
62+
<Dialog.Positioner>
63+
<Dialog.Content p="4">
64+
<form action={handleSubmit}>
65+
<Stack gap="1">
66+
<Dialog.Title textStyle="lg">Contact Us</Dialog.Title>
67+
<Dialog.Description textStyle="sm">
68+
Questions? We'll get back to you as soon as possible.
69+
</Dialog.Description>
70+
</Stack>
8671

87-
<Dialog.CloseTrigger asChild position="absolute" top="2" right="2">
88-
<IconButton aria-label="Close Dialog" variant="ghost" size="sm">
89-
<XIcon />
90-
</IconButton>
91-
</Dialog.CloseTrigger>
92-
</form>
93-
</Dialog.Content>
94-
</Dialog.Positioner>
95-
</Portal>
96-
</Dialog.Root>
97-
)
98-
}
72+
<Stack gap={{ base: "6", md: "8" }}>
73+
<Stack gap="4" pt="5">
74+
<Field.Root required>
75+
<Field.Label>Name</Field.Label>
76+
<Field.Input type="text" asChild>
77+
<Input placeholder="John Doe" name="name" required />
78+
</Field.Input>
79+
</Field.Root>
80+
<Field.Root required>
81+
<Field.Label>E-Mail</Field.Label>
82+
<Field.Input asChild>
83+
<Input
84+
placeholder="me@example.com"
85+
name="email"
86+
type="email"
87+
required
88+
/>
89+
</Field.Input>
90+
</Field.Root>
91+
<Field.Root>
92+
<Field.Label>Message</Field.Label>
93+
<Field.Textarea autoresize asChild>
94+
<Textarea
95+
name="message"
96+
placeholder="How can we assist you?"
97+
required
98+
rows={4}
99+
/>
100+
</Field.Textarea>
101+
</Field.Root>
102+
<Field.Root required>
103+
<Field.Label>
104+
What is {num1} + {num2}?
105+
</Field.Label>
106+
<Field.Input asChild>
107+
<Input
108+
placeholder="Enter the sum"
109+
name="captcha"
110+
type="number"
111+
required
112+
/>
113+
</Field.Input>
114+
</Field.Root>
115+
<input type="hidden" name="subject" value={subject} />
116+
</Stack>
117+
</Stack>
118+
<HStack pt="8" justifyContent="flex-end">
119+
<Button variant="outline" onClick={() => setOpen(false)}>
120+
Cancel
121+
</Button>
122+
<SubmitButton>Submit</SubmitButton>
123+
</HStack>
124+
<Dialog.CloseTrigger
125+
asChild
126+
position="absolute"
127+
top="2"
128+
right="2"
129+
>
130+
<IconButton aria-label="Close Dialog" variant="ghost" size="sm">
131+
<XIcon />
132+
</IconButton>
133+
</Dialog.CloseTrigger>
134+
</form>
135+
</Dialog.Content>
136+
</Dialog.Positioner>
137+
</Portal>
138+
</Dialog.Root>
139+
);
140+
};
99141

100-
export const FloatingContactButton = forwardRef<HTMLButtonElement, IconButtonProps>(
101-
(props, ref) => (
102-
<IconButton
103-
ref={ref}
104-
borderRadius="full"
105-
aria-label="Contact Us"
106-
position="fixed"
107-
bottom="6"
108-
right="6"
109-
boxShadow="lg"
110-
css={{
111-
'& svg': {
112-
width: '6',
113-
height: '6',
114-
},
115-
_hover: {
116-
transform: 'scale(1.1)',
117-
},
118-
}}
119-
size="xl"
120-
{...props}
121-
>
122-
<MessageSquareIcon fill="white" />
123-
</IconButton>
124-
),
125-
)
142+
export const FloatingContactButton = forwardRef<
143+
HTMLButtonElement,
144+
IconButtonProps
145+
>((props, ref) => (
146+
<IconButton
147+
ref={ref}
148+
borderRadius="full"
149+
aria-label="Contact Us"
150+
position="fixed"
151+
bottom="6"
152+
right="6"
153+
boxShadow="lg"
154+
css={{
155+
"& svg": {
156+
width: "6",
157+
height: "6",
158+
},
159+
_hover: {
160+
transform: "scale(1.1)",
161+
},
162+
}}
163+
size="xl"
164+
{...props}
165+
>
166+
<MessageSquareIcon fill="white" />
167+
</IconButton>
168+
));
126169

127-
FloatingContactButton.displayName = 'FloatingContactButton'
170+
FloatingContactButton.displayName = "FloatingContactButton";

0 commit comments

Comments
 (0)