Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/chat interactivity #1196

Merged
merged 5 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@floating-ui/core": "1.2.6",
"@headlessui-float/react": "0.11.2",
"@headlessui/react": "1.7.15",
"@hookform/resolvers": "^3.9.1",
"@mapbox/mapbox-gl-draw": "1.4.1",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
Expand All @@ -37,6 +38,7 @@
"@radix-ui/react-dropdown-menu": "2.0.4",
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "1.0.5",
"@radix-ui/react-progress": "^1.0.2",
"@radix-ui/react-radio-group": "1.1.2",
Expand All @@ -46,6 +48,7 @@
"@radix-ui/react-switch": "^1.0.2",
"@radix-ui/react-toggle": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.5",
"@react-email/components": "^0.0.31",
"@tailwindcss/forms": "0.5.3",
"@tailwindcss/line-clamp": "0.4.2",
"@tailwindcss/typography": "0.5.9",
Expand Down Expand Up @@ -74,20 +77,24 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "14.2.3",
"react-email": "^3.0.4",
"react-hook-form": "^7.54.2",
"react-map-gl": "7.0.21",
"react-markdown": "^9.0.1",
"react-toastify": "^10.0.5",
"react-virtualized": "9.22.5",
"recharts": "^2.5.0",
"recoil": "^0.7.7",
"recoil-sync": "0.2.0",
"resend": "^4.0.1",
"shadcn-ui": "latest",
"tailwind-merge": "1.11.0",
"tailwind-scrollbar-hide": "1.1.7",
"tailwindcss": "3.2.7",
"tailwindcss-animate": "^1.0.5",
"use-debounce": "9.0.3",
"usehooks-ts": "2.9.1"
"usehooks-ts": "2.9.1",
"zod": "^3.24.1"
},
"devDependencies": {
"@playwright/test": "^1.41.0",
Expand Down
52 changes: 52 additions & 0 deletions src/components/contact/email-template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Body, Container, Head, Html, Markdown, Preview, Text } from '@react-email/components';
import { CSSProperties } from 'react';

interface ContactUsEmailProps {
name: string;
email: string;
message: string;
}

export const ContactUsEmail = ({ name, email, message }: ContactUsEmailProps) => (
<Html>
<Head />
<Preview>Thanks for contacting us {name}!</Preview>
<Body style={main}>
<Container style={container}>
<Text style={paragraph}>Hi {name},</Text>
<Text style={paragraph}>We have received your message</Text>
<Markdown
markdownContainerStyles={{
boxShadow: '0 0 10px rgba(0, 0, 0, 0.05)',
borderRadius: '8px',
padding: '20px',
backgroundColor: '#f3f4f6',
border: '1px solid #e5e7eb',
}}
>
{message}
</Markdown>
<Text style={paragraph}>We will get back to you as soon as possible at {email}.</Text>
</Container>
</Body>
</Html>
);

const main: CSSProperties = {
backgroundColor: '#ffffff',
borderRadius: '8px',
border: '1px solid #e5e7eb',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.05)',
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
};

const container: CSSProperties = {
margin: '0 auto',
padding: '20px 0 48px',
};

const paragraph: CSSProperties = {
fontSize: '16px',
lineHeight: '26px',
};
234 changes: 234 additions & 0 deletions src/components/contact/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
'use client';

import { useCallback, useRef, useState } from 'react';

import { useForm } from 'react-hook-form';
import { HiCheck } from 'react-icons/hi2';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// import subscribeNewsletter from '@/containers/newsletter/action';
import cn from 'lib/classnames';
import { HiChevronDown } from 'react-icons/hi';
import { Button } from 'components/ui/button';
import { Checkbox, CheckboxIndicator } from 'components/ui/checkbox';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from 'components/ui/form';
import { Input } from 'components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from 'components/ui/select';
import { Label } from 'components/ui/label';

// import { ContactUsEmail } from './email-template';
// import { postContactForm } from 'services/api';
const TOPICS = [
{ label: 'General', value: 'general' },
{ label: 'Datasets', value: 'datasets' },
{ label: 'GMW Platform', value: 'gmw-platform' },
{ label: 'Mangrove Restoration Tracker Tool', value: 'mrtt' },
{ label: 'Global Mangrove Alliance', value: 'gma' },
] as const;

const TOPICS_VALUES = TOPICS.map((topic) => topic.value) as [string, ...string[]];

export const ContactFormSchema = z.object({
name: z.string({ message: 'Name is required' }).min(2, 'Name must contain at least 2 characters'),
organization: z.string(),
email: z
.string({ message: 'Email is required' })
.min(1, 'Email is required')
.email('Invalid email'),
topic: z.enum(TOPICS_VALUES, { message: 'Please, select a topic' }),
message: z.string().optional(),
});

type FormSchema = z.infer<typeof ContactFormSchema>;

export function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [isOpen, setIsOpen] = useState(false);
const [privacyPolicy, setPrivacyPolicy] = useState(false);

const formRef = useRef<HTMLFormElement>(null);
const form = useForm<z.infer<typeof ContactFormSchema>>({
resolver: zodResolver(ContactFormSchema),
defaultValues: {
name: '',
organization: '',
email: '',
topic: undefined,
message: '',
},
mode: 'onSubmit',
});

const handlePrivacyPolicy = useCallback(() => {
setPrivacyPolicy((prev) => !prev);
}, [setPrivacyPolicy]);

const onSubmitData = async (values: FormSchema) => {
try {
const response = await fetch('api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values), // Send form data to the API
});

if (!response.ok) {
throw new Error(`Failed to send email: ${response.statusText}`);
}

const data = await response.json();
console.info('Email sent successfully:', data);
setStatus('success'); // Update form submission status
} catch (error) {
console.error('Error submitting form:', error);
setStatus('error'); // Update status in case of an error
}
};
return (
<Form {...form}>
<form ref={formRef} onSubmit={form.handleSubmit(onSubmitData)} className="text-black/85">
<div className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="space-y-1.5">
<FormLabel className="text-xs">Name</FormLabel>
<FormControl>
<Input placeholder="Enter your name" type="text" autoComplete="name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="organization"
render={({ field }) => (
<FormItem className="space-y-1.5">
<FormLabel className="text-xs">Organization</FormLabel>
<Input
placeholder="Enter your organization"
type="text"
autoComplete="organization"
{...field}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="topic"
render={({ field }) => (
<FormItem className="space-y-1.5">
<FormLabel className="text-xs">Topics</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
onOpenChange={(open) => setIsOpen(open)}
>
<FormControl>
<SelectTrigger className="focus-visible:ring-ring flex h-9 w-full rounded-3xl border border-black/15 py-0 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand-800 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
<SelectValue placeholder="Select" />
<HiChevronDown
className={cn({
'h-4 w-4': true,
'rotate-180': isOpen,
})}
/>
<span className="sr-only">Select</span>
</SelectTrigger>
</FormControl>
<FormMessage />
<SelectContent className="top-0 w-full rounded-3xl border bg-white p-4 text-sm font-light shadow-sm">
<div className="space-y-4">
{TOPICS.map(({ label, value }) => (
<SelectItem key={value} value={value} className="hover:text-brand-800">
{label}
<span className="sr-only">Select</span>
</SelectItem>
))}
</div>
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="space-y-1.5">
<FormLabel className="text-xs">Email</FormLabel>
<FormControl>
<Input
placeholder="Enter your email"
type="text"
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem className="space-y-1.5">
<FormLabel className="text-xs">Your message</FormLabel>
<FormControl>
<Input placeholder="Enter your email" autoComplete="message" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<button
type="button"
onClick={handlePrivacyPolicy}
className="flex items-center space-x-2.5 text-black/85"
>
<Checkbox
id="privacyPolicy"
className={cn({
'[data=] h-5 w-5 border border-black/15': true,
})}
>
<CheckboxIndicator className="bg-brand-800">
<HiCheck className="stroke-2 text-white" />
</CheckboxIndicator>
</Checkbox>
<Label htmlFor="privacyPolicy" className="cursor-pointer">
I agree with the 
<a href="/" className="underline">
Privacy Policy.
</a>
</Label>
</button>

<div className="space-y-4">
{status === 'loading' && <p>Sending...</p>}
{status === 'success' && <p>Email sent successfully!</p>}
{status === 'error' && <p>Failed to send email. Please try again.</p>}
<Button type="submit" className="h-9 w-full" disabled={false}>
Send message
</Button>
</div>
</div>
</form>
</Form>
);
}

export default ContactForm;
54 changes: 54 additions & 0 deletions src/components/ui/button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';

import cn from 'lib/classnames';

import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-3xl transition-colors text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border h-8',
{
variants: {
variant: {
default: 'bg-brand-800 text-white hover:bg-opacity-90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground border-brand-800/15',
secondary: 'bg-white text-brand-800 hover:bg-accent hover:text-accent-foreground',
ghost: 'bg-brand-800/15 text-black/85 hover:bg-white hover:text-grey-800',
link: 'text-primary rounded-full underline-offset-4 hover:underline',
rounded: 'rounded-full',
},
size: {
default: 'px-4 py-2',
sm: 'px-3',
lg: 'px-5',
xl: 'px-8',
icon: 'h-11 w-11',
none: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
}
);
Button.displayName = 'Button';

export { Button, buttonVariants };
Loading
Loading