diff --git a/.env.development b/.env.development index 2d5dec9..4cdf965 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,2 @@ NEXT_PUBLIC_URL=http://localhost:3000 +NEXT_PUBLIC_FORMSPREE_API_KEY=mjkbwabp diff --git a/.env.production b/.env.production index 60117c7..b47f1a9 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,2 @@ NEXT_PUBLIC_URL=http://www.cesalberca.com +NEXT_PUBLIC_FORMSPREE_API_KEY=mjkbwabp diff --git a/package-lock.json b/package-lock.json index c1ade1b..f7235c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,13 @@ "name": "blog", "version": "0.1.0", "dependencies": { + "@formspree/react": "2.5.1", + "@hookform/resolvers": "^3.9.0", "@icons-pack/react-simple-icons": "9.6.0", "@radix-ui/react-dropdown-menu": "2.1.1", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "1.1.1", - "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-slot": "^1.1.0", "@tailwindcss/typography": "0.5.13", "@uidotdev/usehooks": "2.4.1", "@vercel/analytics": "1.3.1", @@ -28,6 +31,7 @@ "postcss": "8.4.39", "react": "18.3.1", "react-dom": "18.3.1", + "react-hook-form": "^7.52.1", "react-markdown": "9.0.1", "react-syntax-highlighter": "15.5.0", "reflect-metadata": "0.2.2", @@ -35,7 +39,8 @@ "tailwind-merge": "2.4.0", "tailwindcss": "3.4.4", "tailwindcss-animate": "1.0.7", - "tsyringe": "4.8.0" + "tsyringe": "4.8.0", + "zod": "^3.23.8" }, "devDependencies": { "@types/jest": "29.5.12", @@ -881,6 +886,36 @@ "tslib": "^2.4.0" } }, + "node_modules/@formspree/core": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@formspree/core/-/core-3.0.1.tgz", + "integrity": "sha512-jGNgrEtL8mx8k1kkmeNfyeoP1a+H2kci8TMG7CT3R5k3ssqr0Sl1j10SIKo74CFZVewMW4JjMYwcunWUPfutSQ==", + "dependencies": { + "@stripe/stripe-js": "^1.35.0" + } + }, + "node_modules/@formspree/react": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@formspree/react/-/react-2.5.1.tgz", + "integrity": "sha512-Yhxyn32m3zH63i0DNpnBq55zargDlbihqC0wlK18dqU7GOUqJXKY+WywLXuTRGfqo79/hT+Qx6eFS6M0J6eh0A==", + "dependencies": { + "@formspree/core": "^3.0.1", + "@stripe/react-stripe-js": "^1.7.1", + "@stripe/stripe-js": "^1.35.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2017,6 +2052,28 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", + "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", @@ -2382,6 +2439,24 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.16.5.tgz", + "integrity": "sha512-lVPW3IfwdacyS22pP+nBB6/GNFRRhT/4jfgAK6T2guQmtzPwJV1DogiGGaBNhiKtSY18+yS8KlHSu+PvZNclvQ==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz", + "integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -10872,7 +10947,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -10882,8 +10956,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/property-information": { "version": "6.5.0", @@ -11008,6 +11081,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -13620,6 +13708,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 7b749e3..2722717 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,11 @@ "prepare": "husky install" }, "dependencies": { + "@formspree/react": "2.5.1", + "@hookform/resolvers": "3.9.0", "@icons-pack/react-simple-icons": "9.6.0", "@radix-ui/react-dropdown-menu": "2.1.1", + "@radix-ui/react-label": "2.1.0", "@radix-ui/react-popover": "1.1.1", "@radix-ui/react-slot": "1.1.0", "@tailwindcss/typography": "0.5.13", @@ -34,6 +37,7 @@ "postcss": "8.4.39", "react": "18.3.1", "react-dom": "18.3.1", + "react-hook-form": "7.52.1", "react-markdown": "9.0.1", "react-syntax-highlighter": "15.5.0", "reflect-metadata": "0.2.2", @@ -41,7 +45,8 @@ "tailwind-merge": "2.4.0", "tailwindcss": "3.4.4", "tailwindcss-animate": "1.0.7", - "tsyringe": "4.8.0" + "tsyringe": "4.8.0", + "zod": "3.23.8" }, "devDependencies": { "@types/jest": "29.5.12", diff --git a/src/app/globals.css b/src/app/globals.css index 1433ef6..9b7fd32 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -103,7 +103,7 @@ } .wrapper { - @apply mx-auto max-w-xl; + @apply mx-auto max-w-screen-sm; } } diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..7d00fba --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,138 @@ +'use client' + +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form' + +import { cn } from '@/lib/utils' +import { Label } from '@/components/ui/label' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext({} as FormFieldContextValue) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext({} as FormItemContextValue) + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) + }, +) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return