mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 16:36:34 -05:00
docs: improve create sign in box (#7349)
This commit is contained in:
@@ -71,6 +71,33 @@ export function CodeTab({
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{fileName.endsWith(".vue") && (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
className="fill-current"
|
||||
d="M24 1.61h-9.94L12 5.16 9.94 1.61H0l12 20.78ZM12 14.08 5.16 2.23h4.43L12 6.41l2.41-4.18h4.43Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{fileName.endsWith(".svelte") && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
height="1em"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
className="fill-current"
|
||||
d="M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<span className="truncate max-w-[100px]">{fileName}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
596
docs/components/builder/code-tabs/frameworks/nextjs.ts
Normal file
596
docs/components/builder/code-tabs/frameworks/nextjs.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
import { socialProviders } from "../../social-provider";
|
||||
import type { SignInBoxOptions } from "../../store";
|
||||
|
||||
export function resolveNextJSFiles(options: SignInBoxOptions) {
|
||||
const files = [
|
||||
{
|
||||
id: "1",
|
||||
name: "auth.ts",
|
||||
content: `import { betterAuth } from "better-auth";
|
||||
import { nextCookies } from "better-auth/next";${
|
||||
options.magicLink
|
||||
? `
|
||||
import { magicLink } from "better-auth/plugins";`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
import { passkey } from "@better-auth/passkey";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
${
|
||||
options.email
|
||||
? `emailAndPassword: {
|
||||
enabled: true,
|
||||
${
|
||||
options.requestPasswordReset
|
||||
? `async sendResetPassword(data, request) {
|
||||
// Send an email to the user with a link to reset their password
|
||||
},`
|
||||
: ``
|
||||
}
|
||||
},`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders.length
|
||||
? `socialProviders: ${JSON.stringify(
|
||||
options.socialProviders.reduce(
|
||||
(acc, provider) => ({
|
||||
...acc,
|
||||
[provider]: {
|
||||
clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`,
|
||||
clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
).replace(/"/g, "")},`
|
||||
: ""
|
||||
}
|
||||
plugins: [${
|
||||
options.magicLink
|
||||
? `
|
||||
magicLink({
|
||||
async sendMagicLink(data) {
|
||||
// Send an email to the user with a magic link
|
||||
},
|
||||
}),`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
passkey(),`
|
||||
: ""
|
||||
}
|
||||
nextCookies(),
|
||||
],
|
||||
/** if no database is provided, the user data will be stored in memory.
|
||||
* Make sure to provide a database to persist user data **/
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "auth-client.ts",
|
||||
content: `import { createAuthClient } from "better-auth/react";${
|
||||
options.magicLink
|
||||
? `
|
||||
import { magicLinkClient } from "better-auth/client/plugins";`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
import { passkeyClient } from "@better-auth/passkey/client";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_APP_URL,${
|
||||
options.magicLink || options.passkey
|
||||
? `
|
||||
plugins: [${options.magicLink ? `magicLinkClient()${options.passkey ? "," : ""}` : ""}${
|
||||
options.passkey ? `passkeyClient()` : ""
|
||||
}],`
|
||||
: ""
|
||||
}
|
||||
});
|
||||
|
||||
export const { signIn, signOut, signUp, useSession } = authClient;
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "sign-in.tsx",
|
||||
content: signInString(options),
|
||||
},
|
||||
];
|
||||
|
||||
if (options.signUp) {
|
||||
files.push({
|
||||
id: "4",
|
||||
name: "sign-up.tsx",
|
||||
content: signUpString(options),
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const signInString = (options: SignInBoxOptions) => `"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useState } from "react";
|
||||
import { Loader2, Key } from "lucide-react";
|
||||
import { signIn } from "@/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function SignIn() {${
|
||||
options.email || options.magicLink
|
||||
? `
|
||||
const [email, setEmail] = useState("");`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
const [password, setPassword] = useState("");`
|
||||
: ""
|
||||
}
|
||||
const [loading, setLoading] = useState(false);${
|
||||
options.rememberMe
|
||||
? `
|
||||
const [rememberMe, setRememberMe] = useState(false);`
|
||||
: ""
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
${
|
||||
options.email
|
||||
? `<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
value={email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>${
|
||||
options.requestPasswordReset
|
||||
? `
|
||||
<Link
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
autoComplete="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>${
|
||||
options.rememberMe
|
||||
? `
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
onClick={() => {
|
||||
setRememberMe(!rememberMe);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="remember">Remember me</Label>
|
||||
</div>`
|
||||
: ""
|
||||
}`
|
||||
: ""
|
||||
}${
|
||||
options.magicLink
|
||||
? `<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
value={email}
|
||||
/>
|
||||
<Button
|
||||
disabled={loading}
|
||||
className="gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.magicLink({
|
||||
email,
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
):(
|
||||
Sign-in with Magic Link
|
||||
)}
|
||||
</Button>
|
||||
</div>`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signIn.email({
|
||||
email,
|
||||
password,${
|
||||
options.rememberMe
|
||||
? `
|
||||
rememberMe,`
|
||||
: ""
|
||||
}
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<p>Login</p>
|
||||
)}
|
||||
</Button>`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
className="gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.passkey({
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Key size={16} />
|
||||
Sign-in with Passkey
|
||||
</Button>`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders?.length > 0
|
||||
? `
|
||||
<div className={cn(
|
||||
"w-full gap-2 flex items-center",
|
||||
${
|
||||
options.socialProviders.length > 3
|
||||
? '"justify-between flex-wrap"'
|
||||
: '"justify-between flex-col"'
|
||||
}
|
||||
)}>
|
||||
${options.socialProviders
|
||||
.map((provider: string) => {
|
||||
const icon =
|
||||
socialProviders[provider as keyof typeof socialProviders]
|
||||
?.stringIcon || "";
|
||||
return `<Button
|
||||
variant="outline"
|
||||
className=${
|
||||
options.socialProviders.length > 3
|
||||
? '"flex-grow"'
|
||||
: '"w-full gap-2"'
|
||||
}
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "${provider}",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
${
|
||||
options.socialProviders.length <= 3
|
||||
? `Sign in with ${
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1)
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
</Button>`;
|
||||
})
|
||||
.join("\n\t\t\t\t\t\t")}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</CardContent>
|
||||
${
|
||||
options.label
|
||||
? `<CardFooter>
|
||||
<div className="flex justify-center w-full border-t py-4">
|
||||
<p className="text-center text-xs text-neutral-500">
|
||||
built with{" "}
|
||||
<Link
|
||||
href="https://better-auth.com"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="dark:text-white/70 cursor-pointer">
|
||||
better-auth.
|
||||
</span>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}`;
|
||||
|
||||
const signUpString = (options: SignInBoxOptions) => `"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Loader2, X } from "lucide-react";
|
||||
import { signUp } from "@/lib/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function SignUp() {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setImage(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="z-50 rounded-md rounded-t-none max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first-name">First name</Label>
|
||||
<Input
|
||||
id="first-name"
|
||||
placeholder="Max"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setFirstName(e.target.value);
|
||||
}}
|
||||
value={firstName}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last-name">Last name</Label>
|
||||
<Input
|
||||
id="last-name"
|
||||
placeholder="Robinson"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setLastName(e.target.value);
|
||||
}}
|
||||
value={lastName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
value={email}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password_confirmation">Confirm Password</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
value={passwordConfirmation}
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm Password"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="image">Profile Image (optional)</Label>
|
||||
<div className="flex items-end gap-4">
|
||||
{imagePreview && (
|
||||
<div className="relative w-16 h-16 rounded-sm overflow-hidden">
|
||||
<Image
|
||||
src={imagePreview}
|
||||
alt="Profile preview"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="w-full"
|
||||
/>
|
||||
{imagePreview && (
|
||||
<X
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setImage(null);
|
||||
setImagePreview(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signUp.email({
|
||||
email,
|
||||
password,
|
||||
name: \`\${firstName} \${lastName}\`,
|
||||
image: image ? await convertImageToBase64(image) : "",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
"Create your account"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>${
|
||||
options.label
|
||||
? `
|
||||
<CardFooter>
|
||||
<div className="flex justify-center w-full border-t py-4">
|
||||
<p className="text-center text-xs text-neutral-500">
|
||||
Secured by <span className="text-orange-400">better-auth.</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
async function convertImageToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}`;
|
||||
582
docs/components/builder/code-tabs/frameworks/nuxt.ts
Normal file
582
docs/components/builder/code-tabs/frameworks/nuxt.ts
Normal file
@@ -0,0 +1,582 @@
|
||||
import { socialProviders } from "../../social-provider";
|
||||
import type { SignInBoxOptions } from "../../store";
|
||||
|
||||
export function resolveNuxtFiles(options: SignInBoxOptions) {
|
||||
const files = [
|
||||
{
|
||||
id: "1",
|
||||
name: "auth.ts",
|
||||
content: `import { betterAuth } from "better-auth";${
|
||||
options.magicLink
|
||||
? `
|
||||
import { magicLink } from "better-auth/plugins/magic-link";`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
import { passkey } from "@better-auth/passkey";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
${
|
||||
options.email
|
||||
? `emailAndPassword: {
|
||||
enabled: true,
|
||||
${
|
||||
options.requestPasswordReset
|
||||
? `async sendResetPassword(data, request) {
|
||||
// Send an email to the user with a link to reset their password
|
||||
},`
|
||||
: ``
|
||||
}
|
||||
},`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders.length
|
||||
? `socialProviders: ${JSON.stringify(
|
||||
options.socialProviders.reduce(
|
||||
(acc, provider) => ({
|
||||
...acc,
|
||||
[provider]: {
|
||||
clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`,
|
||||
clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
).replace(/"/g, "")},`
|
||||
: ""
|
||||
}${
|
||||
options.magicLink || options.passkey
|
||||
? `
|
||||
plugins: [
|
||||
${
|
||||
options.magicLink
|
||||
? `magicLink({
|
||||
async sendMagicLink(data) {
|
||||
// Send an email to the user with a magic link
|
||||
},
|
||||
}),`
|
||||
: `${options.passkey ? `passkey(),` : ""}`
|
||||
}
|
||||
${options.passkey && options.magicLink ? `passkey(),` : ""}],`
|
||||
: ""
|
||||
}
|
||||
/** if no database is provided, the user data will be stored in memory.
|
||||
* Make sure to provide a database to persist user data **/
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "auth-client.ts",
|
||||
content: `import { createAuthClient } from "better-auth/react";${
|
||||
options.magicLink
|
||||
? `
|
||||
import { magicLinkClient } from "better-auth/client/plugins";`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
import { passkeyClient } from "@better-auth/passkey/client";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NUXT_PUBLIC_APP_URL,${
|
||||
options.magicLink || options.passkey
|
||||
? `
|
||||
plugins: [${options.magicLink ? `magicLinkClient()${options.passkey ? "," : ""}` : ""}${
|
||||
options.passkey ? `passkeyClient()` : ""
|
||||
}],`
|
||||
: ""
|
||||
}
|
||||
});
|
||||
|
||||
export const { signIn, signOut, signUp, useSession } = authClient;
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "sign-in.vue",
|
||||
content: signInString(options),
|
||||
},
|
||||
];
|
||||
if (options.signUp) {
|
||||
files.push({
|
||||
id: "4",
|
||||
name: "sign-up.vue",
|
||||
content: signUpString(options),
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const signInString = (options: SignInBoxOptions) => `<script setup lang="ts">
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { Loader2, Key } from "lucide-vue";
|
||||
import { signIn } from "~/lib/auth-client";
|
||||
import { cn } from "~/lib/utils";
|
||||
${
|
||||
options.email || options.magicLink
|
||||
? `
|
||||
const email = ref("");`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
const password = ref("");`
|
||||
: ""
|
||||
}
|
||||
const loading = ref(false);${
|
||||
options.rememberMe
|
||||
? `
|
||||
const rememberMe = ref(false);`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await signIn.email({
|
||||
email: email.value,
|
||||
password: password.value,${
|
||||
options.rememberMe
|
||||
? `
|
||||
rememberMe: rememberMe.value,`
|
||||
: ""
|
||||
}
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading.value = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
};`
|
||||
: ""
|
||||
}${
|
||||
options.magicLink
|
||||
? `
|
||||
|
||||
const handleMagicLink = async () => {
|
||||
await signIn.magicLink({
|
||||
email: email.value,
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading.value = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
|
||||
const handlePasskey = async () => {
|
||||
await signIn.passkey({
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading.value = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
};`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders.length > 0
|
||||
? `
|
||||
|
||||
const handleSocialSignIn = async (provider: string) => {
|
||||
await signIn.social({
|
||||
provider,
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading.value = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-xs md:text-sm">Sign In</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4">
|
||||
${
|
||||
options.email
|
||||
? `<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
v-model="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center">
|
||||
<Label for="password">Password</Label>${
|
||||
options.requestPasswordReset
|
||||
? `
|
||||
<NuxtLink
|
||||
to="#"
|
||||
class="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</NuxtLink>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
autoComplete="password"
|
||||
v-model="password"
|
||||
/>
|
||||
</div>${
|
||||
options.rememberMe
|
||||
? `
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
v-model="rememberMe"
|
||||
/>
|
||||
<Label for="remember">Remember me</Label>
|
||||
</div>`
|
||||
: ""
|
||||
}`
|
||||
: ""
|
||||
}${
|
||||
options.magicLink
|
||||
? `<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
v-model="email"
|
||||
/>
|
||||
<Button
|
||||
:disabled="loading"
|
||||
class="gap-2"
|
||||
@click="handleMagicLink"
|
||||
>
|
||||
<Loader2 size="16" class="animate-spin" v-if="loading" />
|
||||
<span v-else>
|
||||
Sign-in with Magic Link
|
||||
</span>
|
||||
</Button>
|
||||
</div>`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
:disabled="loading"
|
||||
@click="handleSignIn"
|
||||
>
|
||||
<Loader2 size="16" class="animate-spin" v-if="loading" />
|
||||
<p v-else>Login</p>
|
||||
</Button>`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
<Button
|
||||
variant="secondary"
|
||||
:disabled="loading"
|
||||
class="gap-2"
|
||||
@click="handlePasskey"
|
||||
>
|
||||
<Key size="16" />
|
||||
Sign-in with Passkey
|
||||
</Button>`
|
||||
: ``
|
||||
}${
|
||||
options.socialProviders.length > 0
|
||||
? `
|
||||
<div :class="cn(
|
||||
'w-full gap-2 flex items-center',
|
||||
${
|
||||
options.socialProviders.length > 3
|
||||
? "'justify-between flex-wrap'"
|
||||
: "'justify-between flex-col'"
|
||||
}
|
||||
)">
|
||||
${options.socialProviders
|
||||
.map((provider: string) => {
|
||||
const icon =
|
||||
socialProviders[provider as keyof typeof socialProviders]
|
||||
?.stringIcon || "";
|
||||
return `<Button
|
||||
variant="outline"
|
||||
:class="cn(
|
||||
${
|
||||
options.socialProviders.length > 3
|
||||
? "'flex-grow'"
|
||||
: "'w-full gap-2'"
|
||||
}
|
||||
)"
|
||||
:disabled="loading"
|
||||
@click="handleSocialSignIn('${provider}')"
|
||||
>
|
||||
${icon}
|
||||
${
|
||||
options.socialProviders.length <= 3
|
||||
? `Sign in with ${
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1)
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
</Button>`;
|
||||
})
|
||||
.join("\n\t\t\t\t\t")}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</CardContent>${
|
||||
options.label
|
||||
? `
|
||||
<CardFooter>
|
||||
<div class="flex justify-center w-full border-t py-4">
|
||||
<p class="text-center text-xs text-neutral-500">
|
||||
built with
|
||||
<NuxtLink
|
||||
to="https://better-auth.com"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
<span class="dark:text-white/70 cursor-pointer">
|
||||
better-auth.
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
</template>
|
||||
`;
|
||||
|
||||
const signUpString = (options: SignInBoxOptions) => `<script setup lang="ts">
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Loader2, X } from "lucide-vue";
|
||||
import { signUp } from "~/lib/auth-client";
|
||||
import { toast } from "vue-sonner";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const firstName = ref("");
|
||||
const lastName = ref("");
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const passwordConfirmation = ref("");
|
||||
const image = ref<File | null>(null);
|
||||
const imagePreview = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
async function convertImageToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const handleSignUp = async () => {
|
||||
await signUp.email({
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
name: \`\${firstName.value} \${lastName.value}\`,
|
||||
image: image.value ? await convertImageToBase64(image.value) : "",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onResponse: () => {
|
||||
loading.value = false;
|
||||
},
|
||||
onRequest: () => {
|
||||
loading.value = true;
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
const handleImageChange = (e: Event) => {
|
||||
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||
if (file) {
|
||||
image.value = file;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
imagePreview.value = reader.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="z-50 rounded-md rounded-t-none max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg md:text-xl">Sign Up</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="first-name">First name</Label>
|
||||
<Input
|
||||
id="first-name"
|
||||
placeholder="Max"
|
||||
required
|
||||
v-model="firstName"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last-name">Last name</Label>
|
||||
<Input
|
||||
id="last-name"
|
||||
placeholder="Robinson"
|
||||
required
|
||||
v-model="lastName"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
v-model="email"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
autocomplete="new-password"
|
||||
v-model="password"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm Password</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Confirm Password"
|
||||
v-model="passwordConfirmation"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="image">Profile Image (optional)</Label>
|
||||
<div class="flex items-end gap-4">
|
||||
<div v-if="imagePreview" class="relative w-16 h-16 rounded-sm overflow-hidden">
|
||||
<NuxtImg
|
||||
:src="imagePreview"
|
||||
alt="Profile preview"
|
||||
class="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleImageChange"
|
||||
class="w-full"
|
||||
/>
|
||||
<X
|
||||
class="cursor-pointer"
|
||||
@click="image = null; imagePreview = null"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
:disabled="loading"
|
||||
@click="handleSignUp"
|
||||
>
|
||||
<Loader2 size="16" class="animate-spin" v-if="loading" />
|
||||
<span v-else>Create your account</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
${
|
||||
options.label
|
||||
? `<CardFooter>
|
||||
<div class="flex justify-center w-full border-t py-4">
|
||||
<p class="text-center text-xs text-neutral-500">
|
||||
Secured by <span class="text-orange-400">better-auth.</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
</template>
|
||||
`;
|
||||
577
docs/components/builder/code-tabs/frameworks/svelte-kit.ts
Normal file
577
docs/components/builder/code-tabs/frameworks/svelte-kit.ts
Normal file
@@ -0,0 +1,577 @@
|
||||
import { socialProviders } from "../../social-provider";
|
||||
import type { SignInBoxOptions } from "../../store";
|
||||
|
||||
export function resolveSvelteKitFiles(options: SignInBoxOptions) {
|
||||
const files = [
|
||||
{
|
||||
id: "1",
|
||||
name: "auth.ts",
|
||||
content: `import { betterAuth } from "better-auth";
|
||||
import { sveltekitCookies } from "better-auth/svelte-kit";
|
||||
import { getRequestEvent } from "$app/server";${
|
||||
options.socialProviders.length > 0
|
||||
? `
|
||||
import {
|
||||
${options.socialProviders
|
||||
.map(
|
||||
(provider) => `${provider.toUpperCase()}_CLIENT_ID,
|
||||
${provider.toUpperCase()}_CLIENT_SECRET`,
|
||||
)
|
||||
.join("\n\t")}
|
||||
} from "$env/static/private";`
|
||||
: ""
|
||||
}${
|
||||
options.magicLink
|
||||
? `
|
||||
import { magicLink } from "better-auth/plugins/magic-link";`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
import { passkey } from "@better-auth/passkey";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
${
|
||||
options.email
|
||||
? `emailAndPassword: {
|
||||
enabled: true,
|
||||
${
|
||||
options.requestPasswordReset
|
||||
? `async sendResetPassword(data, request) {
|
||||
// Send an email to the user with a link to reset their password
|
||||
},`
|
||||
: ``
|
||||
}
|
||||
},`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders.length
|
||||
? `socialProviders: ${JSON.stringify(
|
||||
options.socialProviders.reduce(
|
||||
(acc, provider) => ({
|
||||
...acc,
|
||||
[provider]: {
|
||||
clientId: `${provider.toUpperCase()}_CLIENT_ID`,
|
||||
clientSecret: `${provider.toUpperCase()}_CLIENT_SECRET`,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
).replace(/"/g, "")},`
|
||||
: ""
|
||||
}
|
||||
plugins: [${
|
||||
options.magicLink
|
||||
? `
|
||||
magicLink({
|
||||
async sendMagicLink(data) {
|
||||
// Send an email to the user with a magic link
|
||||
},
|
||||
}),`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
passkey(),`
|
||||
: ""
|
||||
}
|
||||
sveltekitCookies(getRequestEvent),
|
||||
],
|
||||
/** if no database is provided, the user data will be stored in memory.
|
||||
* Make sure to provide a database to persist user data **/
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "auth-client.ts",
|
||||
content: `import { createAuthClient } from "better-auth/svelte";
|
||||
import { PUBLIC_APP_URL } from "$env/static/public";${
|
||||
options.magicLink
|
||||
? `
|
||||
import { magicLinkClient } from "better-auth/client/plugins";`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
import { passkeyClient } from "@better-auth/passkey/client";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: PUBLIC_APP_URL,${
|
||||
options.magicLink || options.passkey
|
||||
? `
|
||||
plugins: [${options.magicLink ? `magicLinkClient()${options.passkey ? "," : ""}` : ""}${
|
||||
options.passkey ? `passkeyClient()` : ""
|
||||
}],`
|
||||
: ""
|
||||
}
|
||||
});
|
||||
|
||||
export const { signIn, signOut, signUp, useSession } = authClient;
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "sign-in.svelte",
|
||||
content: signInString(options),
|
||||
},
|
||||
];
|
||||
|
||||
if (options.signUp) {
|
||||
files.push({
|
||||
id: "4",
|
||||
name: "sign-up.svelte",
|
||||
content: signUpString(options),
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const signInString = (options: SignInBoxOptions) => `<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "$lib/components/ui/card/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
|
||||
import { Loader2, Key } from "@lucide/svelte";
|
||||
import { signIn } from "$lib/auth-client.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
${
|
||||
options.email || options.magicLink
|
||||
? `
|
||||
let email = $state("");`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
let password = $state("");`
|
||||
: ""
|
||||
}
|
||||
let loading = $state(false);${
|
||||
options.rememberMe
|
||||
? `
|
||||
let rememberMe = $state(false);`
|
||||
: ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg md:text-xl">Sign In</CardTitle>
|
||||
<CardDescription class="text-xs md:text-sm">
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4">
|
||||
${
|
||||
options.email
|
||||
? `<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
bind:value={email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center">
|
||||
<Label for="password">Password</Label>${
|
||||
options.requestPasswordReset
|
||||
? `
|
||||
<a
|
||||
href="#"
|
||||
class="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
autocomplete="password"
|
||||
bind:value={password}
|
||||
/>
|
||||
</div>${
|
||||
options.rememberMe
|
||||
? `
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
bind:checked={rememberMe}
|
||||
/>
|
||||
<Label for="remember">Remember me</Label>
|
||||
</div>`
|
||||
: ""
|
||||
}`
|
||||
: ""
|
||||
}${
|
||||
options.magicLink
|
||||
? `<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
bind:value={email}
|
||||
/>
|
||||
<Button
|
||||
disabled={loading}
|
||||
onclick={async () => {
|
||||
await signIn.magicLink({
|
||||
email,
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
{:else}
|
||||
<span>Sign-in with Magic Link</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>`
|
||||
: ""
|
||||
}${
|
||||
options.email
|
||||
? `
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
disabled={loading}
|
||||
onclick={async () => {
|
||||
await signIn.email({
|
||||
email,
|
||||
password,${
|
||||
options.rememberMe
|
||||
? `
|
||||
rememberMe,`
|
||||
: ""
|
||||
}
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
{:else}
|
||||
<p>Login</p>
|
||||
{/if}
|
||||
</Button>`
|
||||
: ""
|
||||
}${
|
||||
options.passkey
|
||||
? `
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
class="gap-2"
|
||||
onclick={async () => {
|
||||
await signIn.passkey({
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Key size={16} />
|
||||
Sign-in with Passkey
|
||||
</Button>`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders?.length > 0
|
||||
? `
|
||||
<div class={cn(
|
||||
"w-full gap-2 flex items-center",
|
||||
${
|
||||
options.socialProviders.length > 3
|
||||
? '"justify-between flex-wrap"'
|
||||
: '"justify-between flex-col"'
|
||||
}
|
||||
)}>
|
||||
${options.socialProviders
|
||||
.map((provider: string) => {
|
||||
const icon =
|
||||
socialProviders[provider as keyof typeof socialProviders]
|
||||
?.stringIcon || "";
|
||||
return `<Button
|
||||
variant="outline"
|
||||
class=${
|
||||
options.socialProviders.length > 3
|
||||
? '"flex-grow"'
|
||||
: '"w-full gap-2"'
|
||||
}
|
||||
disabled={loading}
|
||||
onclick={async () => {
|
||||
await signIn.social({
|
||||
provider: "${provider}",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
loading = true;
|
||||
},
|
||||
onResponse: () => {
|
||||
loading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
${
|
||||
options.socialProviders.length <= 3
|
||||
? `Sign in with ${
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1)
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
</Button>`;
|
||||
})
|
||||
.join("\n\t\t\t\t")}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</CardContent>
|
||||
${
|
||||
options.label
|
||||
? `<CardFooter>
|
||||
<div class="flex justify-center w-full border-t py-4">
|
||||
<p class="text-center text-xs text-neutral-500">
|
||||
built with
|
||||
<a
|
||||
href="https://better-auth.com"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="dark:text-white/70 cursor-pointer">
|
||||
better-auth.
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
`;
|
||||
|
||||
const signUpString = (options: SignInBoxOptions) => `<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "$lib/components/ui/card/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import { Loader2, X } from "@lucide/svelte";
|
||||
import { signUp } from "$lib/auth-client.js";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let firstName = $state("");
|
||||
let lastName = $state("");
|
||||
let email = $state("");
|
||||
let password = $state("");
|
||||
let passwordConfirmation = $state("");
|
||||
let image = $state<File | null>(null);
|
||||
let imagePreview = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
|
||||
async function convertImageToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const handleImageChange = (e: Event) => {
|
||||
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||
if (file) {
|
||||
image.value = file;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
imagePreview.value = reader.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Card class="z-50 rounded-md rounded-t-none max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg md:text-xl">Sign Up</CardTitle>
|
||||
<CardDescription class="text-xs md:text-sm">
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="first-name">First Name</Label>
|
||||
<Input
|
||||
id="first-name"
|
||||
placeholder="Max"
|
||||
required
|
||||
bind:value={firstName}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last-name">Last Name</Label>
|
||||
<Input
|
||||
id="last-name"
|
||||
placeholder="Robinson"
|
||||
required
|
||||
bind:value={lastName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
bind:value={email}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
autocomplete="new-password"
|
||||
bind:value={password}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm Password</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
autocomplete="new-password"
|
||||
bind:value={passwordConfirmation}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="image">Profile Image (optional)</Label>
|
||||
<div class="flex items-end gap-4">
|
||||
{#if imagePreview}
|
||||
<div class="relative w-16 h-16 rounded-sm overflow-hidden">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile preview
|
||||
class="object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onchange={handleImageChange}
|
||||
class="w-full"
|
||||
/>
|
||||
{#if imagePreview}
|
||||
<X
|
||||
class="cursor-pointer"
|
||||
onclick={() => {
|
||||
image = null;
|
||||
imagePreview = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
disabled={loading}
|
||||
onclick={async () => {
|
||||
await signUp.email({
|
||||
email,
|
||||
password,
|
||||
name: \`\${firstName} \${lastName}\`,
|
||||
image: image ? await convertImageToBase64(image) : "",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onResponse: () => {
|
||||
loading = false;
|
||||
},
|
||||
onRequest: () => {
|
||||
loading = true;
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
{:else}
|
||||
<span>Create your Account</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>${
|
||||
options.label
|
||||
? `
|
||||
<CardFooter>
|
||||
<div class="flex justify-center w-full border-t py-4">
|
||||
<p class="text-center text-xs text-neutral-500">
|
||||
Secured by <span class="text-orange-400">better-auth.</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>`;
|
||||
@@ -1,112 +1,32 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { js_beautify } from "js-beautify";
|
||||
import { useState } from "react";
|
||||
import { signInString } from "../sign-in";
|
||||
import { signUpString } from "../sign-up";
|
||||
import { useMemo, useState } from "react";
|
||||
import { optionsAtom } from "../store";
|
||||
import { CodeEditor } from "./code-editor";
|
||||
import { resolveNextJSFiles } from "./frameworks/nextjs";
|
||||
import { resolveNuxtFiles } from "./frameworks/nuxt";
|
||||
import { resolveSvelteKitFiles } from "./frameworks/svelte-kit";
|
||||
import { TabBar } from "./tab-bar";
|
||||
|
||||
export default function CodeTabs() {
|
||||
export default function CodeTabs({ framework }: { framework: string }) {
|
||||
const [options] = useAtom(optionsAtom);
|
||||
|
||||
const initialFiles = [
|
||||
{
|
||||
id: "1",
|
||||
name: "auth.ts",
|
||||
content: `import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
${
|
||||
options.email
|
||||
? `emailAndPassword: {
|
||||
enabled: true,
|
||||
${
|
||||
options.requestPasswordReset
|
||||
? `async sendResetPassword(data, request) {
|
||||
// Send an email to the user with a link to reset their password
|
||||
},`
|
||||
: ``
|
||||
}
|
||||
},`
|
||||
: ""
|
||||
}${
|
||||
options.socialProviders.length
|
||||
? `socialProviders: ${JSON.stringify(
|
||||
options.socialProviders.reduce((acc, provider) => {
|
||||
return {
|
||||
...acc,
|
||||
[provider]: {
|
||||
clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`,
|
||||
clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`,
|
||||
},
|
||||
};
|
||||
}, {}),
|
||||
).replace(/"/g, "")},`
|
||||
: ""
|
||||
const files = useMemo(() => {
|
||||
switch (framework) {
|
||||
case "nextjs":
|
||||
return resolveNextJSFiles(options);
|
||||
case "nuxt":
|
||||
return resolveNuxtFiles(options);
|
||||
case "svelte-kit":
|
||||
return resolveSvelteKitFiles(options);
|
||||
case "solid-start":
|
||||
break;
|
||||
}
|
||||
${
|
||||
options.magicLink || options.passkey
|
||||
? `plugins: [
|
||||
${
|
||||
options.magicLink
|
||||
? `magicLink({
|
||||
async sendMagicLink(data) {
|
||||
// Send an email to the user with a magic link
|
||||
},
|
||||
}),`
|
||||
: `${options.passkey ? `passkey(),` : ""}`
|
||||
}
|
||||
${options.passkey && options.magicLink ? `passkey(),` : ""}
|
||||
]`
|
||||
: ""
|
||||
}
|
||||
/** if no database is provided, the user data will be stored in memory.
|
||||
* Make sure to provide a database to persist user data **/
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "auth-client.ts",
|
||||
content: `import { createAuthClient } from "better-auth/react";
|
||||
${
|
||||
options.magicLink || options.passkey
|
||||
? `import { ${options.magicLink ? "magicLinkClient," : ""} ${
|
||||
options.passkey ? "passkeyClient" : ""
|
||||
} } from "better-auth/client/plugins";`
|
||||
: ""
|
||||
}
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
${
|
||||
options.magicLink || options.passkey
|
||||
? `plugins: [${options.magicLink ? `magicLinkClient(),` : ""}${
|
||||
options.passkey ? `passkeyClient(),` : ""
|
||||
}],`
|
||||
: ""
|
||||
}
|
||||
})
|
||||
console.error("Invalid framework", framework);
|
||||
return [];
|
||||
}, [framework, options]);
|
||||
|
||||
export const { signIn, signOut, signUp, useSession } = authClient;
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "sign-in.tsx",
|
||||
content: signInString(options),
|
||||
},
|
||||
];
|
||||
if (options.email) {
|
||||
initialFiles.push({
|
||||
id: "4",
|
||||
name: "sign-up.tsx",
|
||||
content: signUpString(options),
|
||||
});
|
||||
}
|
||||
|
||||
const [files, setFiles] = useState(initialFiles);
|
||||
const [activeFileId, setActiveFileId] = useState(files[0].id);
|
||||
|
||||
const handleTabClick = (fileId: string) => {
|
||||
@@ -114,7 +34,6 @@ ${
|
||||
};
|
||||
|
||||
const handleTabClose = (fileId: string) => {
|
||||
setFiles(files.filter((file) => file.id !== fileId));
|
||||
if (activeFileId === fileId) {
|
||||
setActiveFileId(files[0].id);
|
||||
}
|
||||
@@ -123,7 +42,7 @@ ${
|
||||
const activeFile = files.find((file) => file.id === activeFileId);
|
||||
|
||||
return (
|
||||
<div className="w-full mr-auto max-w-[45rem] mt-8 border border-border rounded-md overflow-hidden">
|
||||
<div className="w-full mr-auto max-w-[60.65rem] mt-8 border border-border rounded-md overflow-hidden">
|
||||
<TabBar
|
||||
files={files}
|
||||
activeFileId={activeFileId}
|
||||
@@ -133,7 +52,7 @@ ${
|
||||
<div className="">
|
||||
{activeFile && (
|
||||
<CodeEditor
|
||||
language="typescript"
|
||||
language={activeFile.name.endsWith(".tsx") ? "tsx" : "typescript"}
|
||||
code={
|
||||
activeFile.name.endsWith(".ts")
|
||||
? js_beautify(activeFile.content)
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { Moon, PlusIcon, Sun } from "lucide-react";
|
||||
import { CircleX, ListFilter, Moon, PlusIcon, Sun, X } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../ui/accordion";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -10,6 +17,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../ui/card";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -18,19 +26,26 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { Switch } from "../ui/switch";
|
||||
import CodeTabs from "./code-tabs";
|
||||
import SignIn from "./sign-in";
|
||||
import { SignUp } from "./sign-up";
|
||||
import { socialProviders } from "./social-provider";
|
||||
import { optionsAtom } from "./store";
|
||||
import { defaultOptions, optionsAtom } from "./store";
|
||||
import { AuthTabs } from "./tabs";
|
||||
|
||||
const frameworks = [
|
||||
const frameworks: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
Icon: React.ComponentType;
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
id: "nextjs",
|
||||
title: "Next.js",
|
||||
description: "The React Framework for Production",
|
||||
Icon: () => (
|
||||
@@ -51,6 +66,7 @@ const frameworks = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "nuxt",
|
||||
title: "Nuxt",
|
||||
description: "The Intuitive Vue Framework",
|
||||
Icon: () => (
|
||||
@@ -72,6 +88,7 @@ const frameworks = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "svelte-kit",
|
||||
title: "SvelteKit",
|
||||
description: "Web development for the rest of us",
|
||||
Icon: () => (
|
||||
@@ -104,8 +121,10 @@ const frameworks = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "solid-start",
|
||||
title: "SolidStart",
|
||||
description: "Fine-grained reactivity goes fullstack",
|
||||
disabled: true,
|
||||
Icon: () => (
|
||||
<svg
|
||||
data-hk="00000010210"
|
||||
@@ -244,10 +263,80 @@ const frameworks = [
|
||||
];
|
||||
|
||||
export function Builder() {
|
||||
const [framework, setFramework] = useState("nextjs");
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const [options, setOptions] = useAtom(optionsAtom);
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
const [socialProviderSearchInput, setSocialProviderSearchInputState] =
|
||||
useState("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
const [debounceTimer, setDebounceTimer] = useState<number | null>(null);
|
||||
|
||||
const setSocialProviderSearchInput = (value: string) => {
|
||||
setSocialProviderSearchInputState(value);
|
||||
|
||||
if (value === "") {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
setDebouncedSearch("");
|
||||
setDebounceTimer(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
const id = window.setTimeout(() => {
|
||||
setDebouncedSearch(value);
|
||||
setDebounceTimer(null);
|
||||
}, 300);
|
||||
|
||||
setDebounceTimer(id);
|
||||
};
|
||||
|
||||
const filteredSocialProviders = useMemo(() => {
|
||||
const providers = Object.entries(socialProviders);
|
||||
if (debouncedSearch.length === 0) {
|
||||
return providers;
|
||||
}
|
||||
|
||||
return providers.filter(([name]) =>
|
||||
name.toLowerCase().includes(debouncedSearch.toLowerCase()),
|
||||
);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const resetSocialProviderSearch = () => {
|
||||
setSocialProviderSearchInput("");
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setOptions(defaultOptions);
|
||||
setFramework("nextjs");
|
||||
};
|
||||
|
||||
const isModified = useMemo(() => {
|
||||
const optionKeys = Object.keys(
|
||||
defaultOptions,
|
||||
) as (keyof typeof defaultOptions)[];
|
||||
return optionKeys.some((key) => {
|
||||
const optionValue = options[key];
|
||||
const defaultValue = defaultOptions[key];
|
||||
if (Array.isArray(optionValue) && Array.isArray(defaultValue)) {
|
||||
if (optionValue.length !== (defaultValue as any).length) {
|
||||
return true;
|
||||
}
|
||||
return optionValue.some(
|
||||
(val, index) => val !== (defaultValue as any)?.[index],
|
||||
);
|
||||
}
|
||||
return optionValue !== defaultValue;
|
||||
});
|
||||
}, [debouncedSearch, options]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -262,8 +351,8 @@ export function Builder() {
|
||||
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-7xl h-5/6 overflow-clip !rounded-none">
|
||||
<DialogHeader>
|
||||
<DialogContent className="@container/create-box px-0! pb-0! max-w-7xl max-h-[85dvh] overflow-clip flex flex-col !rounded-none">
|
||||
<DialogHeader className="px-6">
|
||||
<DialogTitle>Create Sign in Box</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure the sign in box to your liking and copy the code to your
|
||||
@@ -271,334 +360,356 @@ export function Builder() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-scroll no-scrollbar flex gap-4 md:gap-12 flex-col md:flex-row items-center md:items-start">
|
||||
<div className={cn("w-4/12")}>
|
||||
<div className="overflow-scroll h-[580px] relative no-scrollbar">
|
||||
{options.signUp ? (
|
||||
<AuthTabs
|
||||
tabs={[
|
||||
{
|
||||
title: "Sign In",
|
||||
value: "sign-in",
|
||||
content: <SignIn />,
|
||||
},
|
||||
{
|
||||
title: "Sign Up",
|
||||
value: "sign-up",
|
||||
content: <SignUp />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<SignIn />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 grid overflow-y-scroll no-scrollbar @5xl/create-box:grid-cols-9 border-t md:divide-x">
|
||||
<div className="group/preview @5xl/create-box:col-span-4 flex @5xl/create-box:min-h-0 relative">
|
||||
<ScrollArea className="size-full @5xl/create-box:min-h-0 relative">
|
||||
<div className="absolute md:opacity-0 md:group-hover/preview:opacity-100 md:group-focus-within/preview:opacity-100 transition-opacity top-0 right-0 bg-background p-1 z-20">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
onClick={() => {
|
||||
if (resolvedTheme === "dark") {
|
||||
setTheme("light");
|
||||
} else {
|
||||
setTheme("dark");
|
||||
}
|
||||
}}
|
||||
aria-label="Toggle Theme"
|
||||
>
|
||||
{resolvedTheme === "dark" ? (
|
||||
<Moon className="size-3.5" />
|
||||
) : (
|
||||
<Sun className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-6 min-h-0 grid place-items-center">
|
||||
<div className="max-w-md w-full">
|
||||
{options.signUp ? (
|
||||
<AuthTabs
|
||||
tabs={[
|
||||
{
|
||||
title: "Sign In",
|
||||
value: "sign-in",
|
||||
content: <SignIn />,
|
||||
},
|
||||
{
|
||||
title: "Sign Up",
|
||||
value: "sign-up",
|
||||
content: <SignUp />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<SignIn />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<ScrollArea className="w-[45%] flex-grow no-scrollbar">
|
||||
<div className="h-[580px]">
|
||||
{currentStep === 0 ? (
|
||||
<Card className="rounded-none flex-grow h-full">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
<CardTitle>Configuration</CardTitle>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (resolvedTheme === "dark") {
|
||||
setTheme("light");
|
||||
} else {
|
||||
setTheme("dark");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{resolvedTheme === "dark" ? (
|
||||
<Moon onClick={() => setTheme("light")} size={18} />
|
||||
) : (
|
||||
<Sun onClick={() => setTheme("dark")} size={18} />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="max-h-[400px] overflow-scroll">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<Label>Email & Password</Label>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="email-provider-email"
|
||||
>
|
||||
Enabled
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="email-provider-email"
|
||||
checked={options.email}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
email: checked,
|
||||
magicLink: checked ? false : prev.magicLink,
|
||||
signUp: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="email-provider-remember-me"
|
||||
>
|
||||
Remember Me
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="email-provider-remember-me"
|
||||
checked={options.rememberMe}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
rememberMe: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="email-provider-forget-password"
|
||||
>
|
||||
Forget Password
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="email-provider-forget-password"
|
||||
checked={options.requestPasswordReset}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
requestPasswordReset: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div>
|
||||
<Label>Social Providers</Label>
|
||||
</div>
|
||||
<Separator />
|
||||
{Object.entries(socialProviders).map(
|
||||
([provider, { Icon }]) => (
|
||||
<div
|
||||
className="flex items-center justify-between"
|
||||
key={provider}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon />
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor={"social-provider".concat(
|
||||
"-",
|
||||
provider,
|
||||
)}
|
||||
>
|
||||
{provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1)}
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id={"social-provider".concat("-", provider)}
|
||||
checked={options.socialProviders.includes(
|
||||
provider,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
socialProviders: checked
|
||||
? [...prev.socialProviders, provider]
|
||||
: prev.socialProviders.filter(
|
||||
(p) => p !== provider,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div>
|
||||
<Label>Plugins</Label>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 20q-.825 0-1.412-.587T3 18v-.8q0-.85.438-1.562T4.6 14.55q1.55-.775 3.15-1.162T11 13q.35 0 .7.013t.7.062q.275.025.437.213t.163.462q.05 1.175.575 2.213t1.4 1.762q.175.125.275.313t.1.412V19q0 .425-.288.713T14.35 20zm6-8q-1.65 0-2.825-1.175T7 8t1.175-2.825T11 4t2.825 1.175T15 8t-1.175 2.825T11 12m7.5 2q.425 0 .713-.288T19.5 13t-.288-.712T18.5 12t-.712.288T17.5 13t.288.713t.712.287m.15 8.65l-1-1q-.05-.05-.15-.35v-4.45q-1.1-.325-1.8-1.237T15 13.5q0-1.45 1.025-2.475T18.5 10t2.475 1.025T22 13.5q0 1.125-.638 2t-1.612 1.25l.9.9q.15.15.15.35t-.15.35l-.8.8q-.15.15-.15.35t.15.35l.8.8q.15.15.15.35t-.15.35l-1.3 1.3q-.15.15-.35.15t-.35-.15"
|
||||
></path>
|
||||
</svg>
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="plugin-passkey"
|
||||
>
|
||||
Passkey
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="plugin-passkey"
|
||||
checked={options.passkey}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
passkey: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.5 3a4.5 4.5 0 0 1 4.495 4.288L22 7.5V15a2 2 0 0 1-1.85 1.995L20 17h-3v3a1 1 0 0 1-1.993.117L15 20v-3H4a2 2 0 0 1-1.995-1.85L2 15V7.5a4.5 4.5 0 0 1 4.288-4.495L6.5 3zm-11 2A2.5 2.5 0 0 0 4 7.5V15h5V7.5A2.5 2.5 0 0 0 6.5 5M7 8a1 1 0 0 1 .117 1.993L7 10H6a1 1 0 0 1-.117-1.993L6 8z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="plugin-otp-magic-link"
|
||||
>
|
||||
Magic Link
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="plugin-otp-magic-link"
|
||||
checked={options.magicLink}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
magicLink: checked,
|
||||
email: checked ? false : prev.email,
|
||||
signUp: checked ? false : prev.signUp,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="label-powered-by"
|
||||
<div className="@5xl/create-box:col-span-5 @max-5xl/create-box:border-t flex @5xl/create-box:min-h-0">
|
||||
<ScrollArea className="size-full h-full min-h-fit min-h-0 [&_[data-radix-scroll-area-viewport]>div]:min-h-full [&_[data-radix-scroll-area-viewport]>div]:flex! [&_[data-radix-scroll-area-viewport]>div]:flex-col">
|
||||
<Card className="flex flex-col flex-grow h-full rounded-none border-none">
|
||||
{currentStep === 0 && (
|
||||
<>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||
<CardTitle className="py-0.5">Configuration</CardTitle>
|
||||
{isModified && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs px-1.5 py-0.5 hover:text-foreground focus-visible:text-foreground text-muted-foreground transition-colors uppercase"
|
||||
onClick={reset}
|
||||
>
|
||||
Show Built with label
|
||||
</Label>
|
||||
<Switch
|
||||
id="label-powered-by"
|
||||
checked={options.label}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
label: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<button
|
||||
className="bg-stone-950 no-underline group cursor-pointer relative shadow-2xl shadow-zinc-900 rounded-sm p-px text-xs font-semibold leading-6 text-white inline-block w-full"
|
||||
onClick={() => {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}}
|
||||
>
|
||||
<span className="absolute inset-0 overflow-hidden rounded-sm">
|
||||
<span className="absolute inset-0 rounded-sm bg-[image:radial-gradient(75%_100%_at_50%_0%,rgba(56,189,248,0.6)_0%,rgba(56,189,248,0)_75%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100"></span>
|
||||
</span>
|
||||
<div className="relative flex space-x-2 items-center z-10 rounded-none bg-zinc-950 py-2 px-4 ring-1 ring-white/10 justify-center">
|
||||
<span>Continue</span>
|
||||
</div>
|
||||
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
|
||||
</button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
) : currentStep === 1 ? (
|
||||
<Card className="rounded-none flex-grow h-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Choose Framework</CardTitle>
|
||||
<p
|
||||
className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
|
||||
onClick={() => {
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-start gap-2 flex-wrap justify-between">
|
||||
{frameworks.map((fm) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (fm.title === "Next.js") {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-4 border p-6 rounded-md w-5/12 flex-grow h-44 relative",
|
||||
fm.title !== "Next.js"
|
||||
? "opacity-55"
|
||||
: "hover:ring-1 transition-all ring-border hover:bg-background duration-200 ease-in-out cursor-pointer",
|
||||
)}
|
||||
key={fm.title}
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 @container/config">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[
|
||||
"email-and-password",
|
||||
"social-providers",
|
||||
"plugins",
|
||||
]}
|
||||
>
|
||||
{fm.title !== "Next.js" && (
|
||||
<span className="absolute top-4 right-4 text-xs">
|
||||
Coming Soon
|
||||
</span>
|
||||
)}
|
||||
<fm.Icon />
|
||||
<Label className="text-2xl">{fm.title}</Label>
|
||||
<p className="text-sm">{fm.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="rounded-none w-full overflow-y-hidden h-full overflow-auto">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col -mb-2 items-start">
|
||||
<CardTitle>Code</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<p>
|
||||
Copy the code below and paste it in your application to
|
||||
get started.
|
||||
</p>
|
||||
<AccordionItem value="email-and-password">
|
||||
<AccordionTrigger>Email & Password</AccordionTrigger>
|
||||
<AccordionContent className="space-y-2 px-1 pt-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="email-provider-email"
|
||||
>
|
||||
Enabled
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="email-provider-email"
|
||||
checked={options.email}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
email: checked,
|
||||
magicLink: checked ? false : prev.magicLink,
|
||||
signUp: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="email-provider-remember-me"
|
||||
>
|
||||
Remember Me
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="email-provider-remember-me"
|
||||
checked={options.rememberMe}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
rememberMe: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="email-provider-forget-password"
|
||||
>
|
||||
Forget Password
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="email-provider-forget-password"
|
||||
checked={options.requestPasswordReset}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
requestPasswordReset: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="social-providers">
|
||||
<AccordionTrigger>Social providers</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4 px-1 pt-1">
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Filter by name..."
|
||||
className="h-8 ps-9 pe-9"
|
||||
value={socialProviderSearchInput}
|
||||
onChange={(e) =>
|
||||
setSocialProviderSearchInput(e.target.value)
|
||||
}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80">
|
||||
<ListFilter className="size-4" />
|
||||
</div>
|
||||
{socialProviderSearchInput?.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:outline-ring/70 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-lg outline-offset-2 transition-colors focus:z-10 focus-visible:outline focus-visible:outline-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={resetSocialProviderSearch}
|
||||
aria-label="Clear filter"
|
||||
>
|
||||
<CircleX className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2 grid-cols-1 @sm/config:grid-cols-2 @md/config:grid-cols-3 @xl/config:grid-cols-4">
|
||||
{filteredSocialProviders
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([provider, { Icon }]) => (
|
||||
<Label
|
||||
key={provider}
|
||||
htmlFor={"social-provider".concat(
|
||||
"-",
|
||||
provider,
|
||||
)}
|
||||
className={cn(
|
||||
"px-2.5 py-2 rounded-lg border transition-colors flex flex-col items-center justify-center",
|
||||
options.socialProviders.includes(provider)
|
||||
? "border-primary"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<Icon />
|
||||
<span className="text-xs break-all text-center">
|
||||
{provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1)}
|
||||
</span>
|
||||
<Checkbox
|
||||
id={"social-provider".concat(
|
||||
"-",
|
||||
provider,
|
||||
)}
|
||||
className="hidden"
|
||||
checked={options.socialProviders.includes(
|
||||
provider,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
socialProviders: checked
|
||||
? [
|
||||
...prev.socialProviders,
|
||||
provider,
|
||||
]
|
||||
: prev.socialProviders.filter(
|
||||
(p) => p !== provider,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
))}
|
||||
{filteredSocialProviders.length === 0 && (
|
||||
<div className="col-span-full flex flex-col gap-2 items-center justify-center py-10 border border-dashed bg-muted dark:bg-muted/20">
|
||||
No providers found.
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-xs gap-1"
|
||||
onClick={resetSocialProviderSearch}
|
||||
>
|
||||
<X className="-ms-1 size-3.5" />
|
||||
Clear filter
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="plugins" className="border-b!">
|
||||
<AccordionTrigger>Plugins</AccordionTrigger>
|
||||
<AccordionContent className="space-y-2 px-1 pt-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 20q-.825 0-1.412-.587T3 18v-.8q0-.85.438-1.562T4.6 14.55q1.55-.775 3.15-1.162T11 13q.35 0 .7.013t.7.062q.275.025.437.213t.163.462q.05 1.175.575 2.213t1.4 1.762q.175.125.275.313t.1.412V19q0 .425-.288.713T14.35 20zm6-8q-1.65 0-2.825-1.175T7 8t1.175-2.825T11 4t2.825 1.175T15 8t-1.175 2.825T11 12m7.5 2q.425 0 .713-.288T19.5 13t-.288-.712T18.5 12t-.712.288T17.5 13t.288.713t.712.287m.15 8.65l-1-1q-.05-.05-.15-.35v-4.45q-1.1-.325-1.8-1.237T15 13.5q0-1.45 1.025-2.475T18.5 10t2.475 1.025T22 13.5q0 1.125-.638 2t-1.612 1.25l.9.9q.15.15.15.35t-.15.35l-.8.8q-.15.15-.15.35t.15.35l.8.8q.15.15.15.35t-.15.35l-1.3 1.3q-.15.15-.35.15t-.35-.15"
|
||||
></path>
|
||||
</svg>
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="plugin-passkey"
|
||||
>
|
||||
Passkey
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="plugin-passkey"
|
||||
checked={options.passkey}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
passkey: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.5 3a4.5 4.5 0 0 1 4.495 4.288L22 7.5V15a2 2 0 0 1-1.85 1.995L20 17h-3v3a1 1 0 0 1-1.993.117L15 20v-3H4a2 2 0 0 1-1.995-1.85L2 15V7.5a4.5 4.5 0 0 1 4.288-4.495L6.5 3zm-11 2A2.5 2.5 0 0 0 4 7.5V15h5V7.5A2.5 2.5 0 0 0 6.5 5M7 8a1 1 0 0 1 .117 1.993L7 10H6a1 1 0 0 1-.117-1.993L6 8z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="plugin-otp-magic-link"
|
||||
>
|
||||
Magic Link
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="plugin-otp-magic-link"
|
||||
checked={options.magicLink}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
magicLink: checked,
|
||||
email: checked ? false : prev.email,
|
||||
signUp: checked ? false : prev.signUp,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="label-powered-by"
|
||||
>
|
||||
Show Built with label
|
||||
</Label>
|
||||
<Switch
|
||||
id="label-powered-by"
|
||||
checked={options.label}
|
||||
onCheckedChange={(checked) => {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
label: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<button
|
||||
className="bg-stone-950 no-underline group cursor-pointer relative shadow-2xl shadow-zinc-900 rounded-sm p-px text-xs font-semibold leading-6 text-white inline-block w-full"
|
||||
onClick={() => {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}}
|
||||
>
|
||||
<span className="absolute inset-0 overflow-hidden rounded-sm">
|
||||
<span className="absolute inset-0 rounded-sm bg-[image:radial-gradient(75%_100%_at_50%_0%,rgba(56,189,248,0.6)_0%,rgba(56,189,248,0)_75%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100"></span>
|
||||
</span>
|
||||
<div className="relative flex space-x-2 items-center z-10 rounded-none bg-zinc-950 py-2 px-4 ring-1 ring-white/10 justify-center">
|
||||
<span>Continue</span>
|
||||
</div>
|
||||
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
|
||||
</button>
|
||||
</CardFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle>Choose Framework</CardTitle>
|
||||
<p
|
||||
className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -607,15 +718,66 @@ export function Builder() {
|
||||
>
|
||||
Go Back
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<CodeTabs />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-start justify-center gap-2 flex-wrap justify-between">
|
||||
{frameworks.map((fm) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (fm.disabled !== true) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
setFramework(fm.id);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-4 border p-6 rounded-md w-5/12 flex-grow h-44 relative",
|
||||
fm.disabled === true
|
||||
? "opacity-55"
|
||||
: "hover:ring-1 transition-all ring-border hover:bg-background duration-200 ease-in-out cursor-pointer",
|
||||
)}
|
||||
key={fm.id}
|
||||
>
|
||||
{fm.disabled === true && (
|
||||
<span className="absolute top-4 right-4 text-xs">
|
||||
Coming Soon
|
||||
</span>
|
||||
)}
|
||||
<fm.Icon />
|
||||
<Label className="text-2xl">{fm.title}</Label>
|
||||
<p className="text-sm">{fm.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<CardHeader className="flex flex-row justify-between gap-2">
|
||||
<CardTitle>Code</CardTitle>
|
||||
<p
|
||||
className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
|
||||
onClick={() => {
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<p>
|
||||
Copy the code below and paste it in your application
|
||||
to get started.
|
||||
</p>
|
||||
</div>
|
||||
<CodeTabs framework={framework} />
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { optionsAtom } from "./store";
|
||||
export default function SignIn() {
|
||||
const [options] = useAtom(optionsAtom);
|
||||
return (
|
||||
<Card className="z-50 rounded-none max-w-full">
|
||||
<Card className="z-50 rounded-md rounded-t-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
@@ -66,8 +66,8 @@ export default function SignIn() {
|
||||
|
||||
{options.rememberMe && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox />
|
||||
<Label>Remember me</Label>
|
||||
<Checkbox id="remember-me" />
|
||||
<Label htmlFor="remember-me">Remember me</Label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -156,274 +156,3 @@ export default function SignIn() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const signInString = (options: any) => `"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useState } from "react";
|
||||
import { Loader2, Key } from "lucide-react";
|
||||
import { signIn } from "@/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function SignIn() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
${
|
||||
options.rememberMe
|
||||
? "const [rememberMe, setRememberMe] = useState(false);"
|
||||
: ""
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
${
|
||||
options.email
|
||||
? `<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
value={email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
${
|
||||
options.requestPasswordReset
|
||||
? `<Link
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
autoComplete="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
${
|
||||
options.rememberMe
|
||||
? `<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
onClick={() => {
|
||||
setRememberMe(!rememberMe);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="remember">Remember me</Label>
|
||||
</div>`
|
||||
: ""
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
options.magicLink
|
||||
? `<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
value={email}
|
||||
/>
|
||||
<Button
|
||||
disabled={loading}
|
||||
className="gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.magicLink(
|
||||
{
|
||||
email
|
||||
},
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}>
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
):(
|
||||
Sign-in with Magic Link
|
||||
)}
|
||||
</Button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
options.email
|
||||
? `<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signIn.email(
|
||||
{
|
||||
email,
|
||||
password
|
||||
},
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<p> Login </p>
|
||||
)}
|
||||
</Button>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
options.passkey
|
||||
? `<Button
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
className="gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.passkey(
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Key size={16} />
|
||||
Sign-in with Passkey
|
||||
</Button>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
options.socialProviders?.length > 0
|
||||
? `<div className={cn(
|
||||
"w-full gap-2 flex items-center",
|
||||
${
|
||||
options.socialProviders.length > 3
|
||||
? '"justify-between flex-wrap"'
|
||||
: '"justify-between flex-col"'
|
||||
}
|
||||
)}>
|
||||
${options.socialProviders
|
||||
.map((provider: string) => {
|
||||
const icon =
|
||||
socialProviders[provider as keyof typeof socialProviders]
|
||||
?.stringIcon || "";
|
||||
return `\n\t\t\t\t<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
${
|
||||
options.socialProviders.length > 3
|
||||
? '"flex-grow"'
|
||||
: '"w-full gap-2"'
|
||||
}
|
||||
)}
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signIn.social(
|
||||
{
|
||||
provider: "${provider}",
|
||||
callbackURL: "/dashboard"
|
||||
},
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
${
|
||||
options.socialProviders.length <= 3
|
||||
? `Sign in with ${
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1)
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
</Button>`;
|
||||
})
|
||||
.join("")}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</CardContent>
|
||||
${
|
||||
options.label
|
||||
? `<CardFooter>
|
||||
<div className="flex justify-center w-full border-t py-4">
|
||||
<p className="text-center text-xs text-neutral-500">
|
||||
built with{" "}
|
||||
<Link
|
||||
href="https://better-auth.com"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="dark:text-white/70 cursor-pointer">
|
||||
better-auth.
|
||||
</span>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}`;
|
||||
|
||||
@@ -41,7 +41,7 @@ export function SignUp() {
|
||||
const [loading] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="z-50 rounded-md rounded-t-none max-w-md">
|
||||
<Card className="z-50 rounded-md rounded-t-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
@@ -170,210 +170,3 @@ export function SignUp() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const signUpString = (options: any) => `"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Loader2, X } from "lucide-react";
|
||||
import { signUp } from "@/lib/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function SignUp() {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setImage(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="z-50 rounded-md rounded-t-none max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first-name">First name</Label>
|
||||
<Input
|
||||
id="first-name"
|
||||
placeholder="Max"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setFirstName(e.target.value);
|
||||
}}
|
||||
value={firstName}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last-name">Last name</Label>
|
||||
<Input
|
||||
id="last-name"
|
||||
placeholder="Robinson"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setLastName(e.target.value);
|
||||
}}
|
||||
value={lastName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
value={email}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Confirm Password</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
value={passwordConfirmation}
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm Password"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="image">Profile Image (optional)</Label>
|
||||
<div className="flex items-end gap-4">
|
||||
{imagePreview && (
|
||||
<div className="relative w-16 h-16 rounded-sm overflow-hidden">
|
||||
<Image
|
||||
src={imagePreview}
|
||||
alt="Profile preview"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="w-full"
|
||||
/>
|
||||
{imagePreview && (
|
||||
<X
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setImage(null);
|
||||
setImagePreview(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signUp.email({
|
||||
email,
|
||||
password,
|
||||
name: \`\${firstName} \${lastName}\`,
|
||||
image: image ? await convertImageToBase64(image) : "",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
"Create an account"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
${
|
||||
options.label
|
||||
? `<CardFooter>
|
||||
<div className="flex justify-center w-full border-t py-4">
|
||||
<p className="text-center text-xs text-neutral-500">
|
||||
Secured by <span className="text-orange-400">better-auth.</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>`
|
||||
: ""
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
async function convertImageToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}`;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const optionsAtom = atom({
|
||||
export type SignInBoxOptions = typeof defaultOptions;
|
||||
|
||||
export const defaultOptions = {
|
||||
email: true,
|
||||
passkey: false,
|
||||
socialProviders: ["google", "github"],
|
||||
@@ -9,4 +11,6 @@ export const optionsAtom = atom({
|
||||
label: true,
|
||||
rememberMe: true,
|
||||
requestPasswordReset: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const optionsAtom = atom(defaultOptions);
|
||||
|
||||
@@ -36,9 +36,7 @@ export const AuthTabs = ({ tabs: propTabs }: { tabs: Tab[] }) => {
|
||||
onClick={() => {
|
||||
moveSelectedTabToTop(idx);
|
||||
}}
|
||||
className={cn(
|
||||
"relative px-4 py-2 rounded-full opacity-80 hover:opacity-100",
|
||||
)}
|
||||
className={cn("relative px-4 py-2 opacity-80 hover:opacity-100")}
|
||||
>
|
||||
{active.value === tab.value && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user