diff --git a/.idea/prettier.xml b/.idea/prettier.xml index f7ef907..330b69d 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -3,6 +3,6 @@ \ No newline at end of file diff --git a/package.json b/package.json index aef6c3f..78fa372 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,19 @@ }, "dependencies": { "@fontsource/inter": "^5.0.20", + "@hookform/resolvers": "^3.9.0", + "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "lucide-react": "^0.439.0", "next": "14.2.7", "react": "^18", "react-dom": "^18", - "tailwind-merge": "^2.5.2" + "react-hook-form": "^7.53.0", + "tailwind-merge": "^2.5.2", + "zod": "^3.23.8" }, "devDependencies": { "@chromatic-com/storybook": "1.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cb5a3f..8d3d7fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@fontsource/inter': specifier: ^5.0.20 version: 5.0.20 + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.53.0(react@18.3.1)) + '@radix-ui/react-checkbox': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -23,6 +29,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + lucide-react: + specifier: ^0.439.0 + version: 0.439.0(react@18.3.1) next: specifier: 14.2.7 version: 14.2.7(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -32,9 +41,15 @@ importers: react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.53.0 + version: 7.53.0(react@18.3.1) tailwind-merge: specifier: ^2.5.2 version: 2.5.2 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@chromatic-com/storybook': specifier: 1.8.0 @@ -955,6 +970,11 @@ packages: '@fontsource/inter@5.0.20': resolution: {integrity: sha512-rtw2F7xfM7rJmmnncXnR4ADr5wXsp4GyN1O1jmQJ1PMjAK+bm620/ZkQkeOYOkGoa09OksGinOeMA+Mkt6K9PQ==} + '@hookform/resolvers@3.9.0': + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -1233,6 +1253,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.1.1': + resolution: {integrity: sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==} + 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 + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1343,6 +1376,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.0': + resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} + 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 + '@radix-ui/react-primitive@2.0.0': resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: @@ -3657,6 +3703,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.439.0: + resolution: {integrity: sha512-PafSWvDTpxdtNEndS2HIHxcNAbd54OaqSYJO90/b63rab2HWYqDbH194j0i82ZFdWOAcf0AHinRykXRRK2PJbw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -4300,6 +4351,12 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + react-hook-form@7.53.0: + resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -5163,6 +5220,9 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + snapshots: '@adobe/css-tools@4.4.0': {} @@ -6182,6 +6242,10 @@ snapshots: '@fontsource/inter@5.0.20': {} + '@hookform/resolvers@3.9.0(react-hook-form@7.53.0(react@18.3.1))': + dependencies: + react-hook-form: 7.53.0(react@18.3.1) + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6388,6 +6452,22 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-checkbox@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.5)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) @@ -6483,6 +6563,16 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@18.3.1) @@ -9395,6 +9485,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.439.0(react@18.3.1): + dependencies: + react: 18.3.1 + lz-string@1.5.0: {} magic-string@0.30.11: @@ -10055,6 +10149,10 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 + react-hook-form@7.53.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -11064,3 +11162,5 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.1.1: {} + + zod@3.23.8: {} diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..20df71b --- /dev/null +++ b/server/README.md @@ -0,0 +1,15 @@ +# server + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.27. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/server/bun.lockb b/server/bun.lockb new file mode 100644 index 0000000..a287cd4 Binary files /dev/null and b/server/bun.lockb differ diff --git a/server/dashboard.html b/server/dashboard.html new file mode 100644 index 0000000..27e751f --- /dev/null +++ b/server/dashboard.html @@ -0,0 +1,19 @@ + + + + + + + {{username}} | Dashboard + + +

Hello, {{username}}!

+ You are {{age}} and beautiful! + + diff --git a/server/index.html b/server/index.html new file mode 100644 index 0000000..e4028ef --- /dev/null +++ b/server/index.html @@ -0,0 +1,64 @@ + + + + + + + Sign In + + + + +
+
+ + +
+ +
+ + +
+
+ + +
+ + +
+ + + diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..95f2764 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,57 @@ +const users = [ + { + name: 'Andrei', + age: '39', + email: 'andres@gmail.com', + password: 'pass', + }, + + { + name: 'Katya', + age: '18', + email: 'katya@gmail.com', + password: 'another-pass', + }, +] + +Bun.serve({ + port: 3333, + static: { + '/': new Response(await Bun.file('./index.html').bytes(), { + headers: { + 'Content-Type': 'text/html', + }, + }), + }, + async fetch(req) { + const url = new URL(req.url) + if (url.pathname === '/login' && req.method === 'GET') { + const email = url.searchParams.get('email') + const password = url.searchParams.get('password') + + const user = users.find( + (u) => u.email === email && u.password === password + ) + if (!user) { + return new Response(await Bun.file('./login-incorrect.html').bytes(), { + headers: { + 'Content-Type': 'text/html', + }, + }) + } + const file = await Bun.file('./dashboard.html').text() + return new Response( + file + .replaceAll('{{username}}', user.name) + .replaceAll('{{age}}', user.age), + { + headers: { + 'Content-Type': 'text/html', + }, + } + ) + } + if (url.pathname === '/blog') return new Response('Blog!') + return new Response('404!') + }, +}) diff --git a/server/login-incorrect.html b/server/login-incorrect.html new file mode 100644 index 0000000..45831cd --- /dev/null +++ b/server/login-incorrect.html @@ -0,0 +1,21 @@ + + + + + + + Incorrect login + + +

+ Your username or password were incorrect, please try again by + clicking here +

+ + diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..dd3df82 --- /dev/null +++ b/server/package.json @@ -0,0 +1,14 @@ +{ + "name": "server", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "scripts": { + "dev": "bun --watch run index.ts" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/src/components/form/form-checkbox.tsx b/src/components/form/form-checkbox.tsx new file mode 100644 index 0000000..d786a0c --- /dev/null +++ b/src/components/form/form-checkbox.tsx @@ -0,0 +1,35 @@ +import { Checkbox } from '@/components/ui/checkbox/checkbox' +import { ComponentPropsWithoutRef } from 'react' +import { Control, useController, FieldValues, FieldPath } from 'react-hook-form' + +type Props = ComponentPropsWithoutRef< + typeof Checkbox +> & { + control: Control + name: FieldPath +} + +export const FormCheckbox = ({ + control, + name, + errorMessage, + ...props +}: Props) => { + const { + field: { onChange, value, ...field }, + fieldState: { error }, + } = useController({ + control, + name, + }) + + return ( + + ) +} diff --git a/src/components/ui/checkbox/checkbox.tsx b/src/components/ui/checkbox/checkbox.tsx new file mode 100644 index 0000000..ceea949 --- /dev/null +++ b/src/components/ui/checkbox/checkbox.tsx @@ -0,0 +1,39 @@ +'use client' + +import * as React from 'react' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { Check } from 'lucide-react' +import { cn } from '@/utils/cn' +import { ReactNode } from 'react' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + label?: ReactNode + errorMessage?: string + } +>(({ className, label, errorMessage, ...props }, ref) => ( + <> + + {errorMessage &&

{errorMessage}

} + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/text-field/text-field.tsx b/src/components/ui/text-field/text-field.tsx new file mode 100644 index 0000000..be2715f --- /dev/null +++ b/src/components/ui/text-field/text-field.tsx @@ -0,0 +1,42 @@ +import { + ComponentPropsWithoutRef, + ElementRef, + forwardRef, + ReactNode, + useId, +} from 'react' +import { cn } from '@/utils/cn' + +type Props = ComponentPropsWithoutRef<'input'> & { + errorMessage?: string + label?: ReactNode +} + +export const TextField = forwardRef, Props>( + ({ errorMessage, label, className, id, ...rest }, ref) => { + const generatedId = useId() + const idToUse = id ?? generatedId + + return ( +
+ + + {errorMessage && ( +

{errorMessage}

+ )} +
+ ) + } +) + +TextField.displayName = 'TextField' diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 1479bf0..b3340e5 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -1,30 +1,85 @@ -import Link from 'next/link' -import { Button, buttonVariants } from '@/components/ui/button/button' -import * as SelectPrimitive from '@radix-ui/react-select' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { TextField } from '@/components/ui/text-field/text-field' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select/select' +const loginSchema = z.object({ + email: z + .string() + .min(1, 'Required') + .email('Неверный адрес электронной почты'), + password: z + .string({ required_error: 'Required' }) + .min(1, 'Required') + .min(3, 'Минимум 3 символа'), +}) + +const createLoginSchema = (t: ReturnType) => { + return z.object({ + email: z.string().min(1, 'Required').email(t('ERROR_INVALID_EMAIL')), + password: z + .string({ required_error: 'Required' }) + .min(1, 'Required') + .min(3, 'Минимум 3 символа'), + }) +} + +type LoginFields = z.infer export default function Login() { + const t = useI18n() + + const { + handleSubmit, + register, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }) + + const onSubmit = handleSubmit((data) => { + console.log(data) + }) + return ( -
- +
+
+ + + + + +
) } + +const useI18n = () => { + const lang = 'ru' + + return (key: keyof (typeof translations)['ru']) => { + return translations[lang][key] + } +} + +const translations = { + ru: { + ERROR_INVALID_EMAIL: 'Введите валидный адрес эл. почты', + }, + en: { + ERROR_INVALID_EMAIL: 'Invalid email', + }, +} as const diff --git a/src/pages/auth/sign-up.tsx b/src/pages/auth/sign-up.tsx index 17966aa..f1d987d 100644 --- a/src/pages/auth/sign-up.tsx +++ b/src/pages/auth/sign-up.tsx @@ -1,3 +1,106 @@ +import { z } from 'zod' +import { TextField } from '@/components/ui/text-field/text-field' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { FormCheckbox } from '@/components/form/form-checkbox' + +const signUpSchema = z + .object({ + username: z.string(), + email: z.string().email(), + password: z.string(), + passwordConfirmation: z.string(), + agreesToTOS: z.literal(true, { + errorMap: () => ({ message: 'You have to accept our terms of service' }), + }), + }) + .refine((value) => value.password === value.passwordConfirmation, { + message: 'Passwords do not match', + path: ['passwordConfirmation'], + }) + +type SignUpFields = z.infer + export default function SignUp() { - return
SignUp
+ const { + register, + handleSubmit, + formState: { errors }, + control, + } = useForm({ + resolver: zodResolver(signUpSchema), + }) + const onSubmit = handleSubmit((data) => { + console.log(data) + }) + console.log('render') + return ( +
+
+ + + + + +
+ +
+ {/*
*/} + {/* */} + {/* {errors.agreesToTOS && (*/} + {/*

*/} + {/* {errors.agreesToTOS.message}*/} + {/*

*/} + {/* )}*/} + {/*
*/} + + +
+ ) }