[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+const inputGroupAddonVariants = cva(
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ align: {
+ "inline-start":
+ "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
+ "inline-end":
+ "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
+ "block-start":
+ "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
+ "block-end":
+ "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
+ },
+ },
+ defaultVariants: {
+ align: "inline-start",
+ },
+ }
+)
+
+function InputGroupAddon({
+ className,
+ align = "inline-start",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps
) {
+ return (
+ {
+ if ((e.target as HTMLElement).closest("button")) {
+ return
+ }
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
+ }}
+ {...props}
+ />
+ )
+}
+
+const inputGroupButtonVariants = cva(
+ "flex items-center gap-2 text-sm shadow-none",
+ {
+ variants: {
+ size: {
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
+ sm: "",
+ "icon-xs":
+ "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
+ },
+ },
+ defaultVariants: {
+ size: "xs",
+ },
+ }
+)
+
+function InputGroupButton({
+ className,
+ type = "button",
+ variant = "ghost",
+ size = "xs",
+ ...props
+}: Omit, "size"> &
+ VariantProps) {
+ return (
+
+ )
+}
+
+function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function InputGroupInput({
+ className,
+ ...props
+}: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+function InputGroupTextarea({
+ className,
+ ...props
+}: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupText,
+ InputGroupInput,
+ InputGroupTextarea,
+}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..d763cd9
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 0000000..1ac80f7
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import { Label as LabelPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
new file mode 100644
index 0000000..d457090
--- /dev/null
+++ b/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import { Separator as SeparatorPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
new file mode 100644
index 0000000..5e6d7ca
--- /dev/null
+++ b/components/ui/sonner.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import { useTheme } from "@wrksz/themes/client"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+import { HugeiconsIcon } from "@hugeicons/react"
+import { CheckmarkCircle02Icon, InformationCircleIcon, Alert02Icon, MultiplicationSignCircleIcon, Loading03Icon } from "@hugeicons/core-free-icons"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ ),
+ info: (
+
+ ),
+ warning: (
+
+ ),
+ error: (
+
+ ),
+ loading: (
+
+ ),
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ toastOptions={{
+ classNames: {
+ toast: "cn-toast",
+ },
+ }}
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..04d27f7
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/package.json b/package.json
index 9e902e2..547bf97 100644
--- a/package.json
+++ b/package.json
@@ -10,21 +10,25 @@
},
"dependencies": {
"@convex-dev/better-auth": "^0.11.4",
+ "@hookform/resolvers": "^5.2.2",
"@hugeicons/core-free-icons": "^4.1.1",
"@hugeicons/react": "^1.1.6",
+ "@wrksz/themes": "^0.7.9",
"better-auth": "^1.5.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"convex": "^1.34.0",
"next": "16.2.1",
"next-intl": "^4.8.3",
- "next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react-dom": "19.2.4",
+ "react-hook-form": "^7.72.0",
"shadcn": "^4.1.1",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
- "tw-animate-css": "^1.4.0"
+ "tw-animate-css": "^1.4.0",
+ "zod": "^4.3.6"
},
"devDependencies": {
"@better-auth/cli": "^1.4.21",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c97f2e1..0c3cc7c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,12 +11,18 @@ importers:
'@convex-dev/better-auth':
specifier: ^0.11.4
version: 0.11.4(@standard-schema/spec@1.1.0)(better-auth@1.5.6(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(better-sqlite3@12.8.0)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.8.0)(kysely@0.28.14)(pg@8.20.0))(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(convex@1.34.0(react@19.2.4))(hono@4.12.9)(react@19.2.4)(typescript@5.9.3)
+ '@hookform/resolvers':
+ specifier: ^5.2.2
+ version: 5.2.2(react-hook-form@7.72.0(react@19.2.4))
'@hugeicons/core-free-icons':
specifier: ^4.1.1
version: 4.1.1
'@hugeicons/react':
specifier: ^1.1.6
version: 1.1.6(react@19.2.4)
+ '@wrksz/themes':
+ specifier: ^0.7.9
+ version: 0.7.9(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
better-auth:
specifier: ^1.5.6
version: 1.5.6(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(better-sqlite3@12.8.0)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.8.0)(kysely@0.28.14)(pg@8.20.0))(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -35,9 +41,6 @@ importers:
next-intl:
specifier: ^4.8.3
version: 4.8.3(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
- next-themes:
- specifier: ^0.4.6
- version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
radix-ui:
specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -47,15 +50,24 @@ importers:
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
+ react-hook-form:
+ specifier: ^7.72.0
+ version: 7.72.0(react@19.2.4)
shadcn:
specifier: ^4.1.1
version: 4.1.1(@types/node@20.19.37)(typescript@5.9.3)
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwind-merge:
specifier: ^3.5.0
version: 3.5.0
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
devDependencies:
'@better-auth/cli':
specifier: ^1.4.21
@@ -621,6 +633,11 @@ packages:
peerDependencies:
hono: ^4
+ '@hookform/resolvers@5.2.2':
+ resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
+ peerDependencies:
+ react-hook-form: ^7.55.0
+
'@hugeicons/core-free-icons@4.1.1':
resolution: {integrity: sha512-teqIBvPHl90ygIwKyJwTxOH8aNp1X1PjDTcMvLkEwdPxPD+8mssrZ5kXKIAJJFYPsz69a8LYQY0UPid4PAdavg==}
@@ -1784,6 +1801,9 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
'@swc/core-darwin-arm64@1.15.21':
resolution: {integrity: sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==}
engines: {node: '>=10'}
@@ -2169,6 +2189,17 @@ packages:
cpu: [x64]
os: [win32]
+ '@wrksz/themes@0.7.9':
+ resolution: {integrity: sha512-o4/I7JIxKnTmzrnbmNv4o/RG3q4CnX/0m4fG3iRbMGnPamR4ntdzx1qH35XUFmD74NrbEXpEbvIG+BaKvJ2tOA==}
+ peerDependencies:
+ next: '>=16.0.0'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ typescript: '>=4.5.0'
+ peerDependenciesMeta:
+ next:
+ optional: true
+
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -3886,12 +3917,6 @@ packages:
typescript:
optional: true
- next-themes@0.4.6:
- resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
- peerDependencies:
- react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
-
next@16.2.1:
resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==}
engines: {node: '>=20.9.0'}
@@ -4251,6 +4276,12 @@ packages:
peerDependencies:
react: ^19.2.4
+ react-hook-form@7.72.0:
+ resolution: {integrity: sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw==}
+ 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==}
@@ -4471,6 +4502,12 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -5520,6 +5557,11 @@ snapshots:
dependencies:
hono: 4.12.9
+ '@hookform/resolvers@5.2.2(react-hook-form@7.72.0(react@19.2.4))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.72.0(react@19.2.4)
+
'@hugeicons/core-free-icons@4.1.1': {}
'@hugeicons/react@1.1.6(react@19.2.4)':
@@ -6612,6 +6654,8 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@standard-schema/utils@0.3.0': {}
+
'@swc/core-darwin-arm64@1.15.21':
optional: true
@@ -6934,6 +6978,14 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
+ '@wrksz/themes@0.7.9(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)':
+ dependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ typescript: 5.9.3
+ optionalDependencies:
+ next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
@@ -8607,11 +8659,6 @@ snapshots:
transitivePeerDependencies:
- '@swc/helpers'
- next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
- dependencies:
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
-
next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.2.1
@@ -9048,6 +9095,10 @@ snapshots:
react: 19.2.4
scheduler: 0.27.0
+ react-hook-form@7.72.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
react-is@16.13.1: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
@@ -9374,6 +9425,11 @@ snapshots:
sisteransi@1.0.5: {}
+ sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
source-map-js@1.2.1: {}
source-map@0.6.1: {}
diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts
index f0b40bb..919be53 100644
--- a/src/app/api/auth/[...all]/route.ts
+++ b/src/app/api/auth/[...all]/route.ts
@@ -1,3 +1,3 @@
-import { handler } from '@/lib/auth-server';
+import { handler } from '@/src/lib/auth-server';
export const { GET, POST } = handler;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d42b2c9..074e91d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,15 +1,16 @@
-import type { Metadata } from "next";
-import "./globals.css";
-import { NextIntlClientProvider } from "next-intl";
-import { ThemeProvider } from "next-themes";
-import { Geist_Mono } from "next/font/google";
-import { cn } from "@/lib/utils";
+import type { Metadata } from 'next';
+import './globals.css';
+import { NextIntlClientProvider } from 'next-intl';
+import { ThemeProvider } from '@wrksz/themes/next';
+import { Geist_Mono } from 'next/font/google';
+import { cn } from '@/lib/utils';
+import { Toaster } from '@/components/ui/sonner';
-const geistMono = Geist_Mono({subsets:['latin'],variable:'--font-mono'});
+const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-mono' });
export const metadata: Metadata = {
- title: "SaaS Template",
- description: "Create SaaS in a day!",
+ title: 'SaaS Template',
+ description: 'Create SaaS in a day!',
};
export default function RootLayout({
@@ -18,10 +19,17 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
- {children}
+
+ {children}
+
+
diff --git a/src/app/sign-in/page.tsx b/src/app/sign-in/page.tsx
deleted file mode 100644
index 417357c..0000000
--- a/src/app/sign-in/page.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export default function SignIn() {}
diff --git a/src/app/sign-up/page.tsx b/src/app/sign-up/page.tsx
new file mode 100644
index 0000000..46846b8
--- /dev/null
+++ b/src/app/sign-up/page.tsx
@@ -0,0 +1,118 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import {
+ Field,
+ FieldError,
+ FieldGroup,
+ FieldLabel,
+} from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
+import { deafultPasswordValidator } from '@/src/constants';
+import { authClient } from '@/src/lib/auth-client';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Controller, useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod/v4';
+
+const signUpSchema = z.object({
+ email: z.email(),
+ password: deafultPasswordValidator(),
+ name: z.string().min(1).max(100),
+});
+
+export default function SignUp() {
+ const form = useForm>({
+ resolver: zodResolver(signUpSchema),
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ });
+ async function onSubmit(values: z.infer) {
+ const email = values.email;
+ const password = values.password;
+ const name = values.name;
+ await authClient.signUp.email(
+ {
+ email, // user email address
+ password, // user password -> min 8 characters by default
+ name,
+ callbackURL: '/dashboard', // A URL to redirect to after the user verifies their email (optional)
+ },
+ {
+ onRequest: (ctx) => {
+ //show loading
+ },
+ onSuccess: (ctx) => {
+ //redirect to the dashboard or sign in page
+ toast('Registered succesfully');
+ },
+ onError: (ctx) => {
+ // display the error message
+ toast(ctx.error.message);
+ },
+ },
+ );
+ }
+ return (
+
+ );
+}
diff --git a/src/components/core/ThemeChanger.tsx b/src/components/core/ThemeChanger.tsx
index 2c96c1d..629d133 100644
--- a/src/components/core/ThemeChanger.tsx
+++ b/src/components/core/ThemeChanger.tsx
@@ -1,5 +1,5 @@
"use client";
-import { useTheme } from "next-themes";
+import { useTheme } from "@wrksz/themes/client";
import { useEffect, useState } from "react";
export const ThemeChanger = () => {
diff --git a/src/constants.ts b/src/constants.ts
index adaa257..e7d9c98 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1 +1,14 @@
-export const supportedLocales = ["en", "pl"];
+import z from 'zod/v4';
+
+export const supportedLocales = ['en', 'pl'];
+
+export function deafultPasswordValidator() {
+ return z
+ .string()
+ .refine((val) => val.length >= 8, { error: 'Hasło jest za krótkie' })
+ .refine((val) => /[A-Z]/.test(val), { error: 'Wymagana wielka litera' })
+ .refine((val) => /[0-9]/.test(val), { error: 'Wymagana cyfra' })
+ .refine((val) => /[^A-Za-z0-9]/.test(val), {
+ error: 'Wymagany znak specjalny',
+ });
+}