docs: improve create sign in box (#7349)

This commit is contained in:
Joél Solano
2026-01-14 22:22:14 +01:00
committed by GitHub
parent 0c3f608c92
commit 372af1c51e
10 changed files with 2316 additions and 929 deletions

View File

@@ -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>
);

View 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);
});
}`;

View 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>
`;

View 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&nbsp;
<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>`;

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>
);
}`;

View File

@@ -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);
});
}`;

View 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);

View File

@@ -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