diff --git a/package-lock.json b/package-lock.json index f706688..960f165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "golub", "version": "0.0.1", "dependencies": { + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -16,8 +19,10 @@ "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.3", "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", @@ -551,6 +556,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -862,6 +875,44 @@ "node": ">=14" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", + "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -879,6 +930,93 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", + "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -897,6 +1035,93 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz", @@ -953,7 +1178,7 @@ "version": "18.2.17", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -5115,6 +5340,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", + "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6384,6 +6625,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 653f974..3924683 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "cspell": "cspell --show-suggestions --show-context --gitignore ." }, "dependencies": { + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -24,8 +27,10 @@ "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.3", "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index ac94d12..044b950 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -1,24 +1,24 @@ -import FormAuth from '@/components/pages/auth/FormAuth' -import ImageCard from '@/components/pages/auth/ImageCard' +import FormAuth from '@/components/pages/auth/form-auth' +import ImageCard from '@/components/pages/auth/image-card' +import Content from '@/components/ui/content' import IconGolub from '@/components/ui/icons/IconGolub' -import ButtonThemeToggle from '@/components/ui/index/ButtonThemeToggle' -import Layout from '@/components/ui/index/Layout' +import ThemeToggler from '@/components/ui/index/theme-toggler' export default function Auth() { return (
- -
+ +

Authentication

- - + +
- +
) } diff --git a/src/app/globals.css b/src/app/globals.css index 4b77eb9..dfa32ea 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -94,20 +94,4 @@ .caption { @apply text-xs uppercase; } - - .card { - @apply rounded-3xl; - } - - .input-placeholder { - @apply peer-[:not(:placeholder-shown)]:translate-y-0 peer-[:not(:placeholder-shown)]:top-[0.375rem] peer-[:not(:placeholder-shown)]:text-[0.6875rem] peer-[:not(:placeholder-shown)]:leading-[0.8125rem]; - } - - .label-checkbox span { - @apply peer-checked:border-bright-indigo peer-checked:bg-bright-indigo; - } - - .label-checkbox span svg { - @apply peer-checked:opacity-100; - } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 00a24b7..c8354f1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from 'next' import { text, title } from '@/app/fonts' import '@/app/globals.css' -import Providers from '@/store/Providers' +import Providers from '@/store/providers' export const metadata: Metadata = { description: 'Generated by create next app', diff --git a/src/components/pages/auth/FormAuth.tsx b/src/components/pages/auth/FormAuth.tsx deleted file mode 100644 index 8a92e14..0000000 --- a/src/components/pages/auth/FormAuth.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client' - -import { Button } from '@/components/ui/button' -import Form from '@/components/ui/form/Form' -import FormCheckbox from '@/components/ui/form/FormCheckbox' -import FormControl from '@/components/ui/form/FormControl' -import FormInput from '@/components/ui/form/FormInput' -import FormLabel from '@/components/ui/form/FormLabel' -import { FC, FormEvent } from 'react' - -const AuthForm: FC = () => { - const handleFormSubmit = (e: FormEvent) => - console.log(e.target) - - return ( -
- - - Confirm your email and get dynamically generated code - - - - - - I agree to Terms & Conditions and Privacy Policy - - - -
- ) -} - -export default AuthForm diff --git a/src/components/pages/auth/form-auth.tsx b/src/components/pages/auth/form-auth.tsx new file mode 100644 index 0000000..9d85ea2 --- /dev/null +++ b/src/components/pages/auth/form-auth.tsx @@ -0,0 +1,91 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { classnames } from '@/utils' +import { zodResolver } from '@hookform/resolvers/zod' +import { FC } from 'react' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +type FormAuthProps = { className?: string } + +const formSchema = z.object({ + agreement: z.boolean().default(false), + email: z.string().email(), +}) + +const AuthForm: FC = ({ className }) => { + const form = useForm>({ + defaultValues: { + agreement: false, + email: '', + }, + resolver: zodResolver(formSchema), + }) + + function onSubmit(values: z.infer) { + alert(JSON.stringify(values)) + console.log(values) + } + + return ( +
+ + ( + + + Confirm your email and get dynamically generated code + + + + + + + )} + /> + ( + + + + +
+ + I agree to Terms & Conditions and Privacy Policy + + +
+
+ )} + /> + + + + ) +} + +export default AuthForm diff --git a/src/components/pages/auth/ImageCard.tsx b/src/components/pages/auth/image-card.tsx similarity index 77% rename from src/components/pages/auth/ImageCard.tsx rename to src/components/pages/auth/image-card.tsx index 17994e8..839a019 100644 --- a/src/components/pages/auth/ImageCard.tsx +++ b/src/components/pages/auth/image-card.tsx @@ -10,7 +10,7 @@ const ImageCard: FC = ({ children, className }) => { return (
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index f821e7c..f9b6278 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,34 +3,25 @@ import { Slot } from '@radix-ui/react-slot' import { type VariantProps, cva } from 'class-variance-authority' import * as React from 'react' -const buttonVariants = cva( - 'inline-block text-center text-base-white rounded-2xl bg-gradient-purple-blue', - { - defaultVariants: { - size: 'default', - variant: 'default', +const buttonVariants = cva('overflow-hidden inline-block text-center', { + defaultVariants: { + size: 'default', + variant: 'default', + }, + variants: { + size: { + default: 'py-3.5 px-5 rounded-2xl', + icon: 'py-1 px-2 rounded-lg text-xs', }, - variants: { - size: { - default: 'py-3.5 px-5', - icon: 'h-10 w-10', - lg: 'h-11 rounded-md px-8', - sm: 'h-9 rounded-md px-3', - }, - variant: { - default: 'bg-gradient-purple-blue', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - outline: - 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - }, + variant: { + default: + 'relative z-0 text-base-white bg-gradient-purple-blue after:absolute after:-z-[1] after:top-0 after:left-0 after:w-full after:h-full after:bg-gradient-purple-blue-dark after:opacity-0 after:transition-opacity hover:after:opacity-100', + disabled: + 'text-base-gray-4 bg-base-gray-3 dark:text-base-gray-8 dark:bg-base-gray-7', + icon: 'text-base-gray-8 bg-base-gray-2', }, - } -) + }, +}) export interface ButtonProps extends React.ButtonHTMLAttributes, diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..f7d7da0 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +'use client' + +import { cn } from '@/lib/utils' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import * as React from 'react' + +import IconCheck from './icons/IconCheck' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/index/Layout.tsx b/src/components/ui/content.tsx similarity index 68% rename from src/components/ui/index/Layout.tsx rename to src/components/ui/content.tsx index 37c5d47..4c3013e 100644 --- a/src/components/ui/index/Layout.tsx +++ b/src/components/ui/content.tsx @@ -1,12 +1,12 @@ import { classnames } from '@/utils' import { FC, ReactNode } from 'react' -type LayoutProps = { +type ContentProps = { children: ReactNode className?: string } -const Layout: FC = ({ children, className }) => { +const Content: FC = ({ children, className }) => { return (
{children} @@ -14,4 +14,4 @@ const Layout: FC = ({ children, className }) => { ) } -export default Layout +export default Content diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..80a1249 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,175 @@ +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import * as React from 'react' +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form' + +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 { formState, getFieldState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + formDescriptionId: `${id}-form-item-description`, + formItemId: `${id}-form-item`, + formMessageId: `${id}-form-item-message`, + id, + name: fieldContext.name, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { formItemId } = useFormField() + + return ( +