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 (
+
+ )
}