@viliha/vui-ui 1.1.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/AGENT.md +79 -9
  2. package/README.md +60 -0
  3. package/bin/vui.mjs +211 -0
  4. package/package.json +10 -2
  5. package/src/record-view.tsx +59 -17
  6. package/template/.env.example +20 -0
  7. package/template/app/(app)/branches/branches-table.tsx +69 -0
  8. package/template/app/(app)/branches/page.tsx +13 -0
  9. package/template/app/(app)/businesses/businesses-table.tsx +39 -0
  10. package/template/app/(app)/businesses/page.tsx +13 -0
  11. package/template/app/(app)/calendar/page.tsx +725 -0
  12. package/template/app/(app)/charts/charts-content.tsx +190 -0
  13. package/template/app/(app)/charts/layout.tsx +8 -0
  14. package/template/app/(app)/charts/page.tsx +44 -0
  15. package/template/app/(app)/chat/layout.tsx +14 -0
  16. package/template/app/(app)/chat/page.tsx +311 -0
  17. package/template/app/(app)/components/layout.tsx +8 -0
  18. package/template/app/(app)/components/page.tsx +464 -0
  19. package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
  20. package/template/app/(app)/crm/companies/page.tsx +13 -0
  21. package/template/app/(app)/crm/opportunities/opportunities-board.tsx +359 -0
  22. package/template/app/(app)/crm/opportunities/page.tsx +13 -0
  23. package/template/app/(app)/crm/people/page.tsx +13 -0
  24. package/template/app/(app)/crm/people/people-table.tsx +50 -0
  25. package/template/app/(app)/dashboard/layout.tsx +8 -0
  26. package/template/app/(app)/dashboard/page.tsx +239 -0
  27. package/template/app/(app)/departments/departments-table.tsx +55 -0
  28. package/template/app/(app)/departments/page.tsx +13 -0
  29. package/template/app/(app)/employees/employees-table.tsx +64 -0
  30. package/template/app/(app)/employees/page.tsx +13 -0
  31. package/template/app/(app)/forms/layout.tsx +8 -0
  32. package/template/app/(app)/forms/page.tsx +388 -0
  33. package/template/app/(app)/layout.tsx +74 -0
  34. package/template/app/(app)/markets/markets-table.tsx +76 -0
  35. package/template/app/(app)/markets/page.tsx +13 -0
  36. package/template/app/(app)/organizations/edit/page.tsx +72 -0
  37. package/template/app/(app)/organizations/new/page.tsx +46 -0
  38. package/template/app/(app)/organizations/organizations-config.tsx +97 -0
  39. package/template/app/(app)/organizations/organizations-table.tsx +42 -0
  40. package/template/app/(app)/organizations/page.tsx +13 -0
  41. package/template/app/(app)/settings/layout.tsx +8 -0
  42. package/template/app/(app)/settings/page.tsx +255 -0
  43. package/template/app/(app)/steps/page.tsx +263 -0
  44. package/template/app/(app)/support/layout.tsx +14 -0
  45. package/template/app/(app)/support/page.tsx +345 -0
  46. package/template/app/(app)/system/cities/cities-table.tsx +34 -0
  47. package/template/app/(app)/system/cities/page.tsx +13 -0
  48. package/template/app/(app)/system/countries/countries-table.tsx +34 -0
  49. package/template/app/(app)/system/countries/page.tsx +13 -0
  50. package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
  51. package/template/app/(app)/system/currencies/page.tsx +13 -0
  52. package/template/app/(app)/system/languages/languages-table.tsx +32 -0
  53. package/template/app/(app)/system/languages/page.tsx +13 -0
  54. package/template/app/(app)/system/regions/page.tsx +13 -0
  55. package/template/app/(app)/system/regions/regions-table.tsx +32 -0
  56. package/template/app/(app)/users/page.tsx +13 -0
  57. package/template/app/(app)/users/users-table.tsx +97 -0
  58. package/template/app/_components/app-sidebar.tsx +565 -0
  59. package/template/app/_components/auth.tsx +184 -0
  60. package/template/app/_components/breadcrumbs.tsx +26 -0
  61. package/template/app/_components/global-search.tsx +209 -0
  62. package/template/app/_components/logo.tsx +49 -0
  63. package/template/app/_components/nav-config.ts +138 -0
  64. package/template/app/_components/open-tabs.tsx +530 -0
  65. package/template/app/_components/quick-actions.tsx +140 -0
  66. package/template/app/_components/route-meta.ts +137 -0
  67. package/template/app/_components/set-page-title.tsx +15 -0
  68. package/template/app/_components/showcase.tsx +29 -0
  69. package/template/app/_components/stat-card.tsx +56 -0
  70. package/template/app/_components/theme-toggle.tsx +46 -0
  71. package/template/app/_components/top-bar.tsx +109 -0
  72. package/template/app/_components/user-menu.tsx +109 -0
  73. package/template/app/_components/wordmark.tsx +45 -0
  74. package/template/app/apple-icon.png +0 -0
  75. package/template/app/auth/forgot-password/page.tsx +102 -0
  76. package/template/app/auth/layout.tsx +43 -0
  77. package/template/app/auth/page.tsx +13 -0
  78. package/template/app/auth/reset-password/page.tsx +108 -0
  79. package/template/app/auth/signin/page.tsx +238 -0
  80. package/template/app/auth/signup/page.tsx +159 -0
  81. package/template/app/auth/verify/page.tsx +86 -0
  82. package/template/app/error.tsx +56 -0
  83. package/template/app/globals.css +6 -0
  84. package/template/app/icon.png +0 -0
  85. package/template/app/icon.svg +4 -0
  86. package/template/app/layout.tsx +103 -0
  87. package/template/app/not-found.tsx +29 -0
  88. package/template/app/onboarding/layout.tsx +31 -0
  89. package/template/app/onboarding/page.tsx +399 -0
  90. package/template/app/page.tsx +13 -0
  91. package/template/app/register-business/layout.tsx +30 -0
  92. package/template/app/register-business/page.tsx +234 -0
  93. package/template/components/ui/accordion.tsx +66 -0
  94. package/template/components/ui/alert-dialog.tsx +196 -0
  95. package/template/components/ui/alert.tsx +66 -0
  96. package/template/components/ui/aspect-ratio.tsx +11 -0
  97. package/template/components/ui/avatar.tsx +109 -0
  98. package/template/components/ui/badge.tsx +48 -0
  99. package/template/components/ui/breadcrumb.tsx +109 -0
  100. package/template/components/ui/button.tsx +64 -0
  101. package/template/components/ui/calendar.tsx +220 -0
  102. package/template/components/ui/card.tsx +92 -0
  103. package/template/components/ui/checkbox.tsx +32 -0
  104. package/template/components/ui/collapsible.tsx +33 -0
  105. package/template/components/ui/command.tsx +184 -0
  106. package/template/components/ui/dialog.tsx +158 -0
  107. package/template/components/ui/dropdown-menu.tsx +257 -0
  108. package/template/components/ui/form.tsx +167 -0
  109. package/template/components/ui/hover-card.tsx +44 -0
  110. package/template/components/ui/input-otp.tsx +77 -0
  111. package/template/components/ui/input.tsx +21 -0
  112. package/template/components/ui/label.tsx +24 -0
  113. package/template/components/ui/popover.tsx +89 -0
  114. package/template/components/ui/progress.tsx +31 -0
  115. package/template/components/ui/radio-group.tsx +45 -0
  116. package/template/components/ui/scroll-area.tsx +58 -0
  117. package/template/components/ui/select.tsx +190 -0
  118. package/template/components/ui/separator.tsx +28 -0
  119. package/template/components/ui/sheet.tsx +143 -0
  120. package/template/components/ui/skeleton.tsx +13 -0
  121. package/template/components/ui/slider.tsx +63 -0
  122. package/template/components/ui/sonner.tsx +40 -0
  123. package/template/components/ui/switch.tsx +35 -0
  124. package/template/components/ui/table.tsx +116 -0
  125. package/template/components/ui/tabs.tsx +91 -0
  126. package/template/components/ui/textarea.tsx +18 -0
  127. package/template/components/ui/toggle-group.tsx +83 -0
  128. package/template/components/ui/toggle.tsx +47 -0
  129. package/template/components/ui/tooltip.tsx +57 -0
  130. package/template/lib/auth-demo.ts +94 -0
  131. package/template/lib/crm-data.ts +80 -0
  132. package/template/lib/demo-data.ts +267 -0
  133. package/template/lib/mock-data.ts +169 -0
  134. package/template/lib/org-store.ts +50 -0
  135. package/template/lib/seo.ts +188 -0
  136. package/template/lib/utils.ts +3 -0
  137. package/template/next.config.mjs +13 -0
  138. package/template/postcss.config.mjs +7 -0
@@ -0,0 +1,43 @@
1
+ import type { Metadata } from "next";
2
+ import Link from "next/link";
3
+
4
+ import { ThemeToggle } from "@/app/_components/theme-toggle";
5
+ import { Wordmark } from "@/app/_components/wordmark";
6
+ import { FOOTER_NOTICE } from "@/lib/seo";
7
+
8
+ // Auth screens shouldn't surface in search results.
9
+ export const metadata: Metadata = { robots: { index: false, follow: false } };
10
+
11
+ export default function AuthLayout({
12
+ children,
13
+ }: Readonly<{
14
+ children: React.ReactNode;
15
+ }>) {
16
+ return (
17
+ <div className="relative flex min-h-screen flex-col bg-muted/30">
18
+ <div className="absolute right-3 top-3">
19
+ <ThemeToggle />
20
+ </div>
21
+
22
+ <div className="flex flex-1 flex-col items-center justify-center px-4 py-12">
23
+ <div className="w-full max-w-[400px]">
24
+ <Wordmark href="/auth/signin" className="mb-8 justify-center" />
25
+ {children}
26
+ </div>
27
+ </div>
28
+
29
+ <footer className="pb-6 text-center text-muted-foreground">
30
+ <div className="space-x-2">
31
+ <Link href="#" className="hover:text-foreground">
32
+ Terms of Service
33
+ </Link>
34
+ <span aria-hidden="true">·</span>
35
+ <Link href="#" className="hover:text-foreground">
36
+ Privacy Policy
37
+ </Link>
38
+ </div>
39
+ <p className="mt-2">{FOOTER_NOTICE}</p>
40
+ </footer>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ /** The auth section lands on sign-in. Client redirect (static-export safe). */
7
+ export default function AuthIndex() {
8
+ const router = useRouter();
9
+ React.useEffect(() => {
10
+ router.replace("/auth/signin");
11
+ }, [router]);
12
+ return null;
13
+ }
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import {
6
+ ArrowLeftIcon as ArrowLeft,
7
+ CheckCircledIcon as CheckCircle,
8
+ LockClosedIcon as Lock,
9
+ } from "@radix-ui/react-icons";
10
+
11
+ import { Button } from "@viliha/vui-ui/button";
12
+ import { Input } from "@viliha/vui-ui/input";
13
+ import {
14
+ AuthCard,
15
+ AuthCardAside,
16
+ AuthCardBody,
17
+ AuthCardFooter,
18
+ AuthCardHeader,
19
+ Field,
20
+ } from "@/app/_components/auth";
21
+
22
+ export default function ResetPasswordPage() {
23
+ const [password, setPassword] = React.useState("");
24
+ const [confirm, setConfirm] = React.useState("");
25
+ const [error, setError] = React.useState<string>();
26
+ const [done, setDone] = React.useState(false);
27
+
28
+ function submit(e: React.FormEvent) {
29
+ e.preventDefault();
30
+ if (password.length < 8) {
31
+ setError("Password must be at least 8 characters.");
32
+ return;
33
+ }
34
+ if (password !== confirm) {
35
+ setError("Passwords don't match.");
36
+ return;
37
+ }
38
+ setError(undefined);
39
+ setDone(true);
40
+ }
41
+
42
+ if (done) {
43
+ return (
44
+ <AuthCard>
45
+ <AuthCardHeader
46
+ icon={<CheckCircle className="size-6" />}
47
+ title="Password updated"
48
+ description="Your password has been changed. You can sign in now."
49
+ />
50
+ <AuthCardFooter>
51
+ <Link href="/auth/signin" className="block">
52
+ <Button variant="primary" className="w-full">
53
+ Continue to sign in
54
+ </Button>
55
+ </Link>
56
+ </AuthCardFooter>
57
+ </AuthCard>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <AuthCard>
63
+ <form onSubmit={submit}>
64
+ <AuthCardHeader
65
+ title="Set a new password"
66
+ description="Choose a strong password you don't use elsewhere."
67
+ />
68
+ <AuthCardBody>
69
+ <Field label="New password" htmlFor="password" required>
70
+ <Input
71
+ id="password"
72
+ type="password"
73
+ value={password}
74
+ onChange={(e) => setPassword(e.target.value)}
75
+ autoComplete="new-password"
76
+ placeholder="At least 8 characters"
77
+ />
78
+ </Field>
79
+ <Field label="Confirm password" htmlFor="confirm" required error={error}>
80
+ <Input
81
+ id="confirm"
82
+ type="password"
83
+ value={confirm}
84
+ onChange={(e) => setConfirm(e.target.value)}
85
+ autoComplete="new-password"
86
+ placeholder="Re-enter password"
87
+ />
88
+ </Field>
89
+ </AuthCardBody>
90
+ <AuthCardFooter>
91
+ <Button type="submit" className="w-full">
92
+ <Lock className="size-4" />
93
+ Update password
94
+ </Button>
95
+ <AuthCardAside>
96
+ <Link
97
+ href="/auth/signin"
98
+ className="flex items-center justify-center gap-1 hover:text-foreground"
99
+ >
100
+ <ArrowLeft className="size-3.5" />
101
+ Back to sign in
102
+ </Link>
103
+ </AuthCardAside>
104
+ </AuthCardFooter>
105
+ </form>
106
+ </AuthCard>
107
+ );
108
+ }
@@ -0,0 +1,238 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import {
7
+ ArrowLeftIcon as ArrowLeft,
8
+ CheckCircledIcon as ShieldCheck,
9
+ EnvelopeClosedIcon as Mail,
10
+ LockClosedIcon as Fingerprint,
11
+ LockClosedIcon as KeyRound,
12
+ } from "@radix-ui/react-icons";
13
+
14
+ import { Button } from "@viliha/vui-ui/button";
15
+ import { Input } from "@viliha/vui-ui/input";
16
+ import {
17
+ AuthCard,
18
+ AuthCardAside,
19
+ AuthCardBody,
20
+ AuthCardFooter,
21
+ AuthCardHeader,
22
+ Field,
23
+ GoogleIcon,
24
+ OrDivider,
25
+ } from "@/app/_components/auth";
26
+
27
+ type View = "main" | "magic-sent" | "sso" | "2fa";
28
+
29
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
30
+
31
+ export default function SignInPage() {
32
+ const router = useRouter();
33
+ const [view, setView] = React.useState<View>("main");
34
+ const [email, setEmail] = React.useState("");
35
+ const [emailError, setEmailError] = React.useState<string>();
36
+ const [orgId, setOrgId] = React.useState("");
37
+ const [code, setCode] = React.useState("");
38
+ const [busy, setBusy] = React.useState(false);
39
+
40
+ function sendMagicLink(e: React.FormEvent) {
41
+ e.preventDefault();
42
+ if (!EMAIL_RE.test(email.trim())) {
43
+ setEmailError("Enter a valid email address.");
44
+ return;
45
+ }
46
+ setEmailError(undefined);
47
+ setView("magic-sent");
48
+ }
49
+
50
+ function finish() {
51
+ setBusy(true);
52
+ router.push("/dashboard");
53
+ }
54
+
55
+ if (view === "magic-sent") {
56
+ return (
57
+ <AuthCard>
58
+ <AuthCardHeader
59
+ icon={<Mail className="size-6" />}
60
+ title="Check your email"
61
+ description={
62
+ <>
63
+ Sign-in link sent to{" "}
64
+ <span className="font-medium text-foreground">{email}</span>
65
+ </>
66
+ }
67
+ />
68
+ <AuthCardFooter className="space-y-2">
69
+ <Button variant="outline" className="w-full" onClick={() => undefined}>
70
+ Resend link
71
+ </Button>
72
+ <Button variant="ghost" className="w-full" onClick={() => setView("main")}>
73
+ Use a different email
74
+ </Button>
75
+ </AuthCardFooter>
76
+ </AuthCard>
77
+ );
78
+ }
79
+
80
+ if (view === "sso") {
81
+ return (
82
+ <AuthCard>
83
+ <form
84
+ onSubmit={(e) => {
85
+ e.preventDefault();
86
+ finish();
87
+ }}
88
+ >
89
+ <AuthCardHeader title="Single sign-on" />
90
+ <AuthCardBody>
91
+ <Field label="Organization ID" htmlFor="org" required>
92
+ <Input
93
+ id="org"
94
+ value={orgId}
95
+ onChange={(e) => setOrgId(e.target.value)}
96
+ placeholder="org_ACME1234"
97
+ autoComplete="off"
98
+ required
99
+ />
100
+ </Field>
101
+ </AuthCardBody>
102
+ <AuthCardFooter>
103
+ <Button type="submit" className="w-full" disabled={!orgId || busy}>
104
+ Continue to your provider
105
+ </Button>
106
+ <AuthCardAside>
107
+ <BackLink onClick={() => setView("main")} />
108
+ </AuthCardAside>
109
+ </AuthCardFooter>
110
+ </form>
111
+ </AuthCard>
112
+ );
113
+ }
114
+
115
+ if (view === "2fa") {
116
+ return (
117
+ <AuthCard>
118
+ <form
119
+ onSubmit={(e) => {
120
+ e.preventDefault();
121
+ finish();
122
+ }}
123
+ >
124
+ <AuthCardHeader
125
+ icon={<ShieldCheck className="size-6" />}
126
+ title="Two-factor authentication"
127
+ description="Enter your 6-digit code"
128
+ />
129
+ <AuthCardBody>
130
+ <Input
131
+ aria-label="Authentication code"
132
+ value={code}
133
+ onChange={(e) =>
134
+ setCode(e.target.value.replace(/\D/g, "").slice(0, 6))
135
+ }
136
+ inputMode="numeric"
137
+ placeholder="••••••"
138
+ className="text-center text-base tracking-[0.5em]"
139
+ />
140
+ </AuthCardBody>
141
+ <AuthCardFooter>
142
+ <Button
143
+ type="submit"
144
+ className="w-full"
145
+ disabled={code.length !== 6 || busy}
146
+ >
147
+ Verify &amp; sign in
148
+ </Button>
149
+ <AuthCardAside>
150
+ <button
151
+ type="button"
152
+ onClick={finish}
153
+ className="mx-auto block text-primary hover:underline"
154
+ >
155
+ Use a passkey instead
156
+ </button>
157
+ <BackLink onClick={() => setView("main")} />
158
+ </AuthCardAside>
159
+ </AuthCardFooter>
160
+ </form>
161
+ </AuthCard>
162
+ );
163
+ }
164
+
165
+ // main
166
+ return (
167
+ <AuthCard>
168
+ <form onSubmit={sendMagicLink}>
169
+ <AuthCardHeader title="Sign in to your account" />
170
+ <AuthCardBody className="space-y-4">
171
+ <div className="space-y-4">
172
+ <Button
173
+ type="button"
174
+ variant="outline"
175
+ className="w-full"
176
+ onClick={() => setView("2fa")}
177
+ >
178
+ <GoogleIcon />
179
+ Continue with Google
180
+ </Button>
181
+ <Button type="button" variant="outline" className="w-full" onClick={finish}>
182
+ <Fingerprint className="size-4" />
183
+ Sign in with a passkey
184
+ </Button>
185
+ <Button
186
+ type="button"
187
+ variant="outline"
188
+ className="w-full"
189
+ onClick={() => setView("sso")}
190
+ >
191
+ <KeyRound className="size-4" />
192
+ Single sign-on (SSO)
193
+ </Button>
194
+ </div>
195
+
196
+ <OrDivider />
197
+
198
+ <Field label="Work email" htmlFor="email" required error={emailError}>
199
+ <Input
200
+ id="email"
201
+ type="email"
202
+ value={email}
203
+ onChange={(e) => setEmail(e.target.value)}
204
+ placeholder="you@company.com"
205
+ autoComplete="email"
206
+ />
207
+ </Field>
208
+ </AuthCardBody>
209
+ <AuthCardFooter>
210
+ <Button type="submit" className="w-full">
211
+ <Mail className="size-4" />
212
+ Email me a magic link
213
+ </Button>
214
+ <AuthCardAside>
215
+ New to Vui Starter?{" "}
216
+ <Link href="/auth/signup" className="font-medium text-primary hover:underline">
217
+ Create an account
218
+ </Link>
219
+ </AuthCardAside>
220
+ </AuthCardFooter>
221
+ </form>
222
+ </AuthCard>
223
+ );
224
+ }
225
+
226
+ /** Small "Back" link shared by the SSO / 2FA sub-views. */
227
+ function BackLink({ onClick }: { onClick: () => void }) {
228
+ return (
229
+ <button
230
+ type="button"
231
+ onClick={onClick}
232
+ className="flex w-full items-center justify-center gap-1 text-muted-foreground hover:text-foreground"
233
+ >
234
+ <ArrowLeft className="size-3.5" />
235
+ Back
236
+ </button>
237
+ );
238
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import {
7
+ CheckIcon as Check,
8
+ EnvelopeOpenIcon as MailCheck,
9
+ QuestionMarkCircledIcon as ShieldQuestion,
10
+ } from "@radix-ui/react-icons";
11
+
12
+ import { cn } from "@/lib/utils";
13
+ import { Button } from "@viliha/vui-ui/button";
14
+ import { Input } from "@viliha/vui-ui/input";
15
+ import {
16
+ AuthCard,
17
+ AuthCardAside,
18
+ AuthCardBody,
19
+ AuthCardFooter,
20
+ AuthCardHeader,
21
+ Field,
22
+ GoogleIcon,
23
+ OrDivider,
24
+ } from "@/app/_components/auth";
25
+ import { checkBusinessEmail } from "@/lib/auth-demo";
26
+
27
+ export default function SignUpPage() {
28
+ const router = useRouter();
29
+ const [email, setEmail] = React.useState("");
30
+ const [error, setError] = React.useState<string>();
31
+ const [robot, setRobot] = React.useState(false);
32
+ const [sent, setSent] = React.useState(false);
33
+
34
+ function submit(e: React.FormEvent) {
35
+ e.preventDefault();
36
+ const check = checkBusinessEmail(email);
37
+ if (!check.ok) {
38
+ setError(
39
+ check.reason === "public"
40
+ ? "Please use your work email — personal domains (Gmail, Outlook, …) aren't allowed."
41
+ : "Enter a valid email address.",
42
+ );
43
+ return;
44
+ }
45
+ if (!robot) {
46
+ setError("Please confirm you're not a robot.");
47
+ return;
48
+ }
49
+ setError(undefined);
50
+ setSent(true);
51
+ }
52
+
53
+ if (sent) {
54
+ return (
55
+ <AuthCard>
56
+ <AuthCardHeader
57
+ icon={<MailCheck className="size-6" />}
58
+ title="Verify your email"
59
+ description={
60
+ <>
61
+ Verification link sent to{" "}
62
+ <span className="font-medium text-foreground">{email}</span>
63
+ </>
64
+ }
65
+ />
66
+ <AuthCardFooter className="space-y-2">
67
+ {/* Demo shortcut — no real inbox in the demo. */}
68
+ <Button className="w-full" onClick={() => router.push("/onboarding")}>
69
+ I&apos;ve verified — continue
70
+ </Button>
71
+ <Button variant="ghost" className="w-full" onClick={() => setSent(false)}>
72
+ Change email
73
+ </Button>
74
+ </AuthCardFooter>
75
+ </AuthCard>
76
+ );
77
+ }
78
+
79
+ return (
80
+ <AuthCard>
81
+ <form onSubmit={submit}>
82
+ <AuthCardHeader title="Create your account" />
83
+ <AuthCardBody className="space-y-4">
84
+ <Button
85
+ type="button"
86
+ variant="outline"
87
+ className="w-full"
88
+ onClick={() => router.push("/onboarding")}
89
+ >
90
+ <GoogleIcon />
91
+ Sign up with Google
92
+ </Button>
93
+
94
+ <OrDivider />
95
+
96
+ <Field label="Work email" htmlFor="email" required error={error}>
97
+ <Input
98
+ id="email"
99
+ type="email"
100
+ value={email}
101
+ onChange={(e) => setEmail(e.target.value)}
102
+ placeholder="you@company.com"
103
+ autoComplete="email"
104
+ />
105
+ </Field>
106
+
107
+ <RecaptchaMock checked={robot} onChange={setRobot} />
108
+ </AuthCardBody>
109
+ <AuthCardFooter>
110
+ <Button type="submit" className="w-full">
111
+ Create account
112
+ </Button>
113
+ <AuthCardAside>
114
+ Already have an account?{" "}
115
+ <Link href="/auth/signin" className="font-medium text-primary hover:underline">
116
+ Sign in
117
+ </Link>
118
+ </AuthCardAside>
119
+ </AuthCardFooter>
120
+ </form>
121
+ </AuthCard>
122
+ );
123
+ }
124
+
125
+ /** Non-functional reCAPTCHA look-alike for the demo (frontend spam gate). */
126
+ function RecaptchaMock({
127
+ checked,
128
+ onChange,
129
+ }: {
130
+ checked: boolean;
131
+ onChange: (v: boolean) => void;
132
+ }) {
133
+ return (
134
+ <div className="flex items-center justify-between rounded-md border border-input bg-background px-3 py-2.5">
135
+ <div className="flex items-center gap-2.5">
136
+ <button
137
+ type="button"
138
+ role="checkbox"
139
+ aria-checked={checked}
140
+ aria-label="I'm not a robot"
141
+ onClick={() => onChange(!checked)}
142
+ className={cn(
143
+ "grid size-5 place-items-center rounded-[3px] border transition-colors",
144
+ checked
145
+ ? "border-primary bg-primary text-primary-foreground"
146
+ : "border-muted-foreground/40 bg-background",
147
+ )}
148
+ >
149
+ {checked && <Check className="size-3.5" />}
150
+ </button>
151
+ I&apos;m not a robot
152
+ </div>
153
+ <div className="flex flex-col items-center text-muted-foreground">
154
+ <ShieldQuestion className="size-5" aria-hidden="true" />
155
+ <span className="text-xs leading-tight">reCAPTCHA</span>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { ArrowLeftIcon as ArrowLeft, LockClosedIcon as Shield } from "@radix-ui/react-icons";
7
+
8
+ import { Button } from "@viliha/vui-ui/button";
9
+ import {
10
+ AuthCard,
11
+ AuthCardAside,
12
+ AuthCardBody,
13
+ AuthCardFooter,
14
+ AuthCardHeader,
15
+ } from "@/app/_components/auth";
16
+ import {
17
+ InputOTP,
18
+ InputOTPGroup,
19
+ InputOTPSlot,
20
+ } from "@/components/ui/input-otp";
21
+
22
+ export default function VerifyPage() {
23
+ const router = useRouter();
24
+ const [code, setCode] = React.useState("");
25
+ const [error, setError] = React.useState<string>();
26
+
27
+ function submit(e: React.FormEvent) {
28
+ e.preventDefault();
29
+ if (code.length !== 6) {
30
+ setError("Enter all 6 digits.");
31
+ return;
32
+ }
33
+ setError(undefined);
34
+ router.push("/dashboard");
35
+ }
36
+
37
+ return (
38
+ <AuthCard>
39
+ <form onSubmit={submit}>
40
+ <AuthCardHeader
41
+ icon={<Shield className="size-6" />}
42
+ title="Enter verification code"
43
+ description="We sent a 6-digit code to your email."
44
+ />
45
+ <AuthCardBody className="flex flex-col items-center gap-4">
46
+ <InputOTP
47
+ maxLength={6}
48
+ value={code}
49
+ onChange={(v) => {
50
+ setCode(v);
51
+ setError(undefined);
52
+ }}
53
+ >
54
+ <InputOTPGroup>
55
+ {Array.from({ length: 6 }, (_, i) => (
56
+ <InputOTPSlot key={i} index={i} />
57
+ ))}
58
+ </InputOTPGroup>
59
+ </InputOTP>
60
+ {error && <p className="text-destructive">{error}</p>}
61
+ </AuthCardBody>
62
+ <AuthCardFooter>
63
+ <Button type="submit" className="w-full" disabled={code.length !== 6}>
64
+ Verify
65
+ </Button>
66
+ <AuthCardAside>
67
+ <button
68
+ type="button"
69
+ onClick={() => setCode("")}
70
+ className="mx-auto block hover:text-foreground"
71
+ >
72
+ Didn&apos;t get a code? Resend
73
+ </button>
74
+ <Link
75
+ href="/auth/signin"
76
+ className="flex items-center justify-center gap-1 hover:text-foreground"
77
+ >
78
+ <ArrowLeft className="size-3.5" />
79
+ Back to sign in
80
+ </Link>
81
+ </AuthCardAside>
82
+ </AuthCardFooter>
83
+ </form>
84
+ </AuthCard>
85
+ );
86
+ }