Skip to content

Commit

Permalink
feat: add react and form contact with function
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoineGerv committed Feb 14, 2023
1 parent 704c673 commit 58754f5
Show file tree
Hide file tree
Showing 63 changed files with 1,273 additions and 259 deletions.
5 changes: 4 additions & 1 deletion astro.config.mjs
Expand Up @@ -10,6 +10,7 @@ import mdx from '@astrojs/mdx';
import partytown from '@astrojs/partytown';
import compress from 'astro-compress';
import NetlifyCMS from 'astro-netlify-cms';
import react from '@astrojs/react';

import { SITE } from './src/config.mjs';

Expand Down Expand Up @@ -59,6 +60,8 @@ export default defineConfig({
logger: 1,
}),

react(),

NetlifyCMS({
previewStyles: ['https://fonts.googleapis.com/css2?family=Roboto&display=swap', '/src/assets/styles/cms.css'],
config: {
Expand Down Expand Up @@ -157,7 +160,7 @@ export default defineConfig({
vite: {
resolve: {
alias: {
'~': path.resolve(__dirname, './src'),
'@': path.resolve(__dirname, './src'),
},
},
},
Expand Down
6 changes: 2 additions & 4 deletions netlify.toml
@@ -1,7 +1,5 @@
[build]
publish = "dist"
command = "npm run build"
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[functions]
directory = './src/functions'
18 changes: 11 additions & 7 deletions package.json
Expand Up @@ -15,21 +15,22 @@
},
"devDependencies": {
"@astrojs/image": "0.13.0",
"@astrojs/mdx": "^0.15.0",
"@astrojs/mdx": "^0.15.2",
"@astrojs/partytown": "^1.0.3",
"@astrojs/rss": "^2.1.0",
"@astrojs/sitemap": "^1.0.0",
"@astrojs/sitemap": "^1.0.1",
"@astrojs/tailwind": "3.0.0",
"@astrolib/analytics": "^0.3.0",
"@astrolib/seo": "^0.3.0",
"@fontsource/inter": "^4.5.15",
"@netlify/functions": "^1.4.0",
"@tailwindcss/typography": "^0.5.9",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"astro": "^2.0.2",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"astro": "^2.0.11",
"astro-compress": "1.1.28",
"astro-icon": "^0.8.0",
"eslint": "^8.33.0",
"eslint": "^8.34.0",
"eslint-plugin-astro": "^0.23.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"limax": "^2.1.0",
Expand All @@ -39,14 +40,17 @@
"sharp": "^0.31.3",
"subfont": "^6.12.4",
"svgo": "2.8.0",
"tailwindcss": "^3.2.4",
"tailwindcss": "^3.2.6",
"typescript": "^4.9.5"
},
"engines": {
"node": ">=16.12.0"
},
"dependencies": {
"@astrojs/react": "^2.0.2",
"astro-netlify-cms": "^0.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"swiper": "^9.0.5"
}
}
2 changes: 1 addition & 1 deletion src/components/Logo.astro
Expand Up @@ -2,5 +2,5 @@
---

<span class="self-center ml-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white">
🚀 AstroWind</span
🪚 Charpente Bois Debout</span
>
39 changes: 39 additions & 0 deletions src/components/avis/Reviews.tsx
@@ -0,0 +1,39 @@
import { useState, useEffect } from 'react';

const Reviews = ({ placeId }) => {

const ApiKeyGoogleMaps = process.env.GOOGLE_MAPS_API_KEY;

const [reviews, setReviews] = useState([]);

useEffect(() => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${ApiKeyGoogleMaps}&libraries=places`;
script.async = true;
document.body.appendChild(script);

script.onload = () => {
const service = new window.google.maps.places.PlacesService(document.createElement('div'));

service.getDetails({ placeId }, (place, status) => {
if (status === 'OK') {
setReviews(place.reviews);
}
});
};
}, [placeId]);

return (
<div>
{reviews.map((review) => (
<div key={review.time}>
<p>{review.text}</p>
<p>By {review.author_name}</p>
<p>Rating: {review.rating}</p>
</div>
))}
</div>
);
};

export default Reviews;
2 changes: 1 addition & 1 deletion src/components/common/BasicScripts.astro
@@ -1,5 +1,5 @@
---
import { SITE } from '~/config.mjs';
import { SITE } from '@/config.mjs';
---

<script is:inline define:vars={{ defaultTheme: SITE.defaultTheme }}>
Expand Down
10 changes: 5 additions & 5 deletions src/components/common/MetaTags.astro
Expand Up @@ -3,12 +3,12 @@ import { AstroSeo } from '@astrolib/seo';
import { GoogleAnalytics } from '@astrolib/analytics';
import { getImage } from '@astrojs/image';
import { SITE } from '~/config.mjs';
import { MetaSEO } from '~/types';
import { getCanonical, getAsset } from '~/utils/permalinks';
import { getRelativeUrlByFilePath } from '~/utils/directories';
import { SITE } from '@/config.mjs';
import { MetaSEO } from '@/types';
import { getCanonical, getAsset } from '@/utils/permalinks';
import { getRelativeUrlByFilePath } from '@/utils/directories';
import CustomStyles from '~/components/CustomStyles.astro';
import CustomStyles from '@/components/CustomStyles.astro';
import SplitbeeAnalytics from './SplitbeeAnalytics.astro';
export interface Props extends MetaSEO {
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/ToggleTheme.astro
@@ -1,7 +1,7 @@
---
import { Icon } from 'astro-icon';
import { SITE } from '~/config.mjs';
import { SITE } from '@/config.mjs';
export interface Props {
label?: string;
Expand Down
124 changes: 124 additions & 0 deletions src/components/contact/ContactForm.tsx
@@ -0,0 +1,124 @@

import { useForm } from '@/utils/useForm';
import sanitize from '@/utils/sanitize';
import validations from '@/utils/validations';
import { useState } from 'react';
import Input from './Input';
import PulseSpinner from './PulseSpinner';
import TextArea from './TextArea';

export default function ContactForm() {
const [serverError, setServerError] = useState('')
const { submitting, submitted, register, handleSubmit } = useForm<Contact>({
validations,
sanitizeFn: sanitize,
onSubmit: async (data) => {
try {
const res = await fetch('/.netlify/functions/contact', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
})

if (!res.ok) {
throw new Error(JSON.stringify(await res.json()))
}

return true
} catch (_err) {
const err = _err as Error
console.error(err)
setServerError('Something went wrong, please try again later')
return false
}
},
})

return (
<div className="container max-w-sm sm:max-w-lg my-2 lg:my-0 bg-gray-100">
<div className="form-container relative w-full rounded-xl shadow-lg bg-blackAlpha-600">
<div
className={[
'form-overlay',
'absolute',
'h-full',
'inset-0',
'rounded-xl',
'z-20',
'bg-gray-900',
'opacity-70',
submitting ? 'centered' : 'hidden',
].join(' ')}
>
<PulseSpinner />
</div>

<form
onSubmit={handleSubmit}
noValidate
className="relative w-full p-4 px-6 sm:py-6 md:py-8 md:px-10"
>
<div className="pb-6 px-12 text-center">
<span className="text-gray-600 font-semibold text-lg text-center">
Enter your contact info and we will get back to as soon as possible!
</span>
</div>
{serverError && (
<div className="mb-4 sm:mb-6 bg-red-500 py-4 rounded-md text-center">
<h3 className="text2xl text-white">{serverError}</h3>
</div>
)}
{submitted && (
<div className="mb-4 sm:mb-6 bg-green-500 py-4 rounded-md text-center">
<h3 className="text2xl text-white">Message successfully sent!</h3>
</div>
)}
<div className="flex flex-col gap-2">
<Input
name="firstName"
label="First name"
required
bg="bg-gray-100"
{...register('firstName')}
/>
<Input
name="lastName"
label="Last name"
required
bg="bg-gray-100"
{...register('lastName')}
/>
<Input
name="email"
label="Email address"
required
bg="bg-gray-100"
{...register('email')}
/>
<Input name="phone" label="Phone number" bg="bg-gray-100" {...register('phone')} />
<TextArea
name="message"
label="Message"
height="h-48"
required
bg="bg-gray-100"
{...register('message')}
/>
</div>

<div className="py-4 mt-4">
<button
type="submit"
disabled={submitting || submitted}
className="w-full px-2 py-4 rounded-md text-white text-lg font-semibold uppercase bg-green-600 hover:bg-green-700 disabled:bg-green-900 disabled:text-gray-300"
>
Send message
</button>
</div>
</form>
</div>
</div>
)
}
99 changes: 99 additions & 0 deletions src/components/contact/Input.tsx
@@ -0,0 +1,99 @@
export type InputProps = {
name: string
label: string
type?: 'text' | 'email' | 'password'
placeholder?: boolean
required?: boolean
defaultValue?: string
size?: 'sm' | 'md' | 'lg'
bg?: string // The background color of the input's background
onChange?: (e?: any) => void
onBlur?: (e?: any) => void
error?: string
color?: 'light' | 'dark'
}

export default function Input({
name,
label,
type = 'text',
placeholder,
required = false,
defaultValue = '',
size = 'md',
onChange,
onBlur,
error = '',
color = 'light',
bg,
}: InputProps) {
return (
<div className="text-input">
<div className="relative mt-2 w-full">
<input
type={type}
id={name}
placeholder={(placeholder || label) + (required ? '*' : '')}
defaultValue={defaultValue}
onInput={onChange}
onBlur={onBlur}
className={[
'peer',
'block',
'w-full',
'appearance-none',
'rounded-lg',
'border',
'border-1',
Boolean(error) ? 'border-red-400' : 'border-gray-300',
'bg-transparent',
'px-2.5',
'pb-2.5',
'pt-4',
size === 'sm' ? 'text-sm' : size === 'md' ? 'text-md' : 'text-lg',
color === 'light' ? 'text-gray-800' : 'text-gray-50',
Boolean(error) ? 'focus:border-red-400' : 'focus:border-green-600',
'focus:outline-none',
'focus:ring-0',
].join(' ')}
/>
<label
htmlFor={name}
className={[
'absolute',
'top-2',
'left-1',
'z-10',
'origin-[0]',
'-translate-y-5',
'scale-75',
'transform',
'cursor-text',
'select-none',
bg ? bg : 'bg-purple-500', // The bg must be the color of the input's background
'px-2',
size === 'sm' ? 'text-sm' : size === 'md' ? 'text-md' : 'text-lg',
Boolean(error) ? 'text-red-400' : 'text-gray-500',
'duration-300',
'peer-placeholder-shown:top-1/2',
'peer-placeholder-shown:-translate-y-1/2',
'peer-placeholder-shown:scale-100',
'peer-focus:top-2',
'peer-focus:-translate-y-5',
'peer-focus:scale-75',
'peer-focus:px-2',
Boolean(error) ? 'focus:text-red-400' : 'peer-focus:text-green-600',
].join(' ')}
>
{label + (required ? '*' : '')}
</label>
</div>

{error && (
<div className="ml-2">
<span className="text-sm text-red-400">{error}</span>
</div>
)}
</div>
)
}

0 comments on commit 58754f5

Please sign in to comment.