@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.
- package/AGENT.md +79 -9
- package/README.md +60 -0
- package/bin/vui.mjs +211 -0
- package/package.json +10 -2
- package/src/record-view.tsx +59 -17
- package/template/.env.example +20 -0
- package/template/app/(app)/branches/branches-table.tsx +69 -0
- package/template/app/(app)/branches/page.tsx +13 -0
- package/template/app/(app)/businesses/businesses-table.tsx +39 -0
- package/template/app/(app)/businesses/page.tsx +13 -0
- package/template/app/(app)/calendar/page.tsx +725 -0
- package/template/app/(app)/charts/charts-content.tsx +190 -0
- package/template/app/(app)/charts/layout.tsx +8 -0
- package/template/app/(app)/charts/page.tsx +44 -0
- package/template/app/(app)/chat/layout.tsx +14 -0
- package/template/app/(app)/chat/page.tsx +311 -0
- package/template/app/(app)/components/layout.tsx +8 -0
- package/template/app/(app)/components/page.tsx +464 -0
- package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
- package/template/app/(app)/crm/companies/page.tsx +13 -0
- package/template/app/(app)/crm/opportunities/opportunities-board.tsx +359 -0
- package/template/app/(app)/crm/opportunities/page.tsx +13 -0
- package/template/app/(app)/crm/people/page.tsx +13 -0
- package/template/app/(app)/crm/people/people-table.tsx +50 -0
- package/template/app/(app)/dashboard/layout.tsx +8 -0
- package/template/app/(app)/dashboard/page.tsx +239 -0
- package/template/app/(app)/departments/departments-table.tsx +55 -0
- package/template/app/(app)/departments/page.tsx +13 -0
- package/template/app/(app)/employees/employees-table.tsx +64 -0
- package/template/app/(app)/employees/page.tsx +13 -0
- package/template/app/(app)/forms/layout.tsx +8 -0
- package/template/app/(app)/forms/page.tsx +388 -0
- package/template/app/(app)/layout.tsx +74 -0
- package/template/app/(app)/markets/markets-table.tsx +76 -0
- package/template/app/(app)/markets/page.tsx +13 -0
- package/template/app/(app)/organizations/edit/page.tsx +72 -0
- package/template/app/(app)/organizations/new/page.tsx +46 -0
- package/template/app/(app)/organizations/organizations-config.tsx +97 -0
- package/template/app/(app)/organizations/organizations-table.tsx +42 -0
- package/template/app/(app)/organizations/page.tsx +13 -0
- package/template/app/(app)/settings/layout.tsx +8 -0
- package/template/app/(app)/settings/page.tsx +255 -0
- package/template/app/(app)/steps/page.tsx +263 -0
- package/template/app/(app)/support/layout.tsx +14 -0
- package/template/app/(app)/support/page.tsx +345 -0
- package/template/app/(app)/system/cities/cities-table.tsx +34 -0
- package/template/app/(app)/system/cities/page.tsx +13 -0
- package/template/app/(app)/system/countries/countries-table.tsx +34 -0
- package/template/app/(app)/system/countries/page.tsx +13 -0
- package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
- package/template/app/(app)/system/currencies/page.tsx +13 -0
- package/template/app/(app)/system/languages/languages-table.tsx +32 -0
- package/template/app/(app)/system/languages/page.tsx +13 -0
- package/template/app/(app)/system/regions/page.tsx +13 -0
- package/template/app/(app)/system/regions/regions-table.tsx +32 -0
- package/template/app/(app)/users/page.tsx +13 -0
- package/template/app/(app)/users/users-table.tsx +97 -0
- package/template/app/_components/app-sidebar.tsx +565 -0
- package/template/app/_components/auth.tsx +184 -0
- package/template/app/_components/breadcrumbs.tsx +26 -0
- package/template/app/_components/global-search.tsx +209 -0
- package/template/app/_components/logo.tsx +49 -0
- package/template/app/_components/nav-config.ts +138 -0
- package/template/app/_components/open-tabs.tsx +530 -0
- package/template/app/_components/quick-actions.tsx +140 -0
- package/template/app/_components/route-meta.ts +137 -0
- package/template/app/_components/set-page-title.tsx +15 -0
- package/template/app/_components/showcase.tsx +29 -0
- package/template/app/_components/stat-card.tsx +56 -0
- package/template/app/_components/theme-toggle.tsx +46 -0
- package/template/app/_components/top-bar.tsx +109 -0
- package/template/app/_components/user-menu.tsx +109 -0
- package/template/app/_components/wordmark.tsx +45 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/auth/forgot-password/page.tsx +102 -0
- package/template/app/auth/layout.tsx +43 -0
- package/template/app/auth/page.tsx +13 -0
- package/template/app/auth/reset-password/page.tsx +108 -0
- package/template/app/auth/signin/page.tsx +238 -0
- package/template/app/auth/signup/page.tsx +159 -0
- package/template/app/auth/verify/page.tsx +86 -0
- package/template/app/error.tsx +56 -0
- package/template/app/globals.css +6 -0
- package/template/app/icon.png +0 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +103 -0
- package/template/app/not-found.tsx +29 -0
- package/template/app/onboarding/layout.tsx +31 -0
- package/template/app/onboarding/page.tsx +399 -0
- package/template/app/page.tsx +13 -0
- package/template/app/register-business/layout.tsx +30 -0
- package/template/app/register-business/page.tsx +234 -0
- package/template/components/ui/accordion.tsx +66 -0
- package/template/components/ui/alert-dialog.tsx +196 -0
- package/template/components/ui/alert.tsx +66 -0
- package/template/components/ui/aspect-ratio.tsx +11 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +48 -0
- package/template/components/ui/breadcrumb.tsx +109 -0
- package/template/components/ui/button.tsx +64 -0
- package/template/components/ui/calendar.tsx +220 -0
- package/template/components/ui/card.tsx +92 -0
- package/template/components/ui/checkbox.tsx +32 -0
- package/template/components/ui/collapsible.tsx +33 -0
- package/template/components/ui/command.tsx +184 -0
- package/template/components/ui/dialog.tsx +158 -0
- package/template/components/ui/dropdown-menu.tsx +257 -0
- package/template/components/ui/form.tsx +167 -0
- package/template/components/ui/hover-card.tsx +44 -0
- package/template/components/ui/input-otp.tsx +77 -0
- package/template/components/ui/input.tsx +21 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/popover.tsx +89 -0
- package/template/components/ui/progress.tsx +31 -0
- package/template/components/ui/radio-group.tsx +45 -0
- package/template/components/ui/scroll-area.tsx +58 -0
- package/template/components/ui/select.tsx +190 -0
- package/template/components/ui/separator.tsx +28 -0
- package/template/components/ui/sheet.tsx +143 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/slider.tsx +63 -0
- package/template/components/ui/sonner.tsx +40 -0
- package/template/components/ui/switch.tsx +35 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +91 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +83 -0
- package/template/components/ui/toggle.tsx +47 -0
- package/template/components/ui/tooltip.tsx +57 -0
- package/template/lib/auth-demo.ts +94 -0
- package/template/lib/crm-data.ts +80 -0
- package/template/lib/demo-data.ts +267 -0
- package/template/lib/mock-data.ts +169 -0
- package/template/lib/org-store.ts +50 -0
- package/template/lib/seo.ts +188 -0
- package/template/lib/utils.ts +3 -0
- package/template/next.config.mjs +13 -0
- 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 & 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'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'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'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
|
+
}
|