feat: sign-in box

This commit is contained in:
Bereket Engida
2024-11-23 02:31:03 +03:00
parent f80ba1d9be
commit 2ca5c094a5
24 changed files with 2182 additions and 1424 deletions

View File

@@ -0,0 +1,61 @@
"use client";
import React, { useState } from "react";
import { Highlight, themes } from "prism-react-renderer";
import { Check, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import theme from "./theme";
interface CodeEditorProps {
code: string;
language: string;
}
export function CodeEditor({ code, language }: CodeEditorProps) {
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(code);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
return (
<div className="relative">
<Highlight theme={theme} code={code} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
className={`${className} p-4 overflow-auto max-h-[400px] rounded-md`}
style={style}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
<span className="mr-4 text-gray-500 select-none">{i + 1}</span>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
<Button
variant="outline"
size="icon"
className="absolute top-2 right-2"
onClick={copyToClipboard}
aria-label="Copy code"
>
{isCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
);
}

View File

@@ -0,0 +1,55 @@
import React from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
interface TabProps {
fileName: string;
isActive: boolean;
brightnessLevel?: number; // New optional prop for brightness level
onClick: () => void;
onClose: () => void;
}
const brightnessLevels = [
"bg-background",
"bg-background-200", //
"bg-background-300",
"bg-background-400",
"bg-background-500",
"bg-background-600",
"bg-background-700",
];
export function CodeTab({
fileName,
isActive,
brightnessLevel = 0,
onClick,
onClose,
}: TabProps) {
const activeBrightnessClass = isActive
? brightnessLevels[brightnessLevel % brightnessLevels.length]
: "bg-muted";
const textColor = isActive ? "text-foreground" : "text-muted-foreground";
const borderColor = isActive
? "border-t-foreground"
: "border-t-transparent hover:bg-background/50";
return (
<div
className={cn(
"flex items-center px-3 py-2 text-sm font-medium border-t-2 cursor-pointer transition-colors duration-200",
activeBrightnessClass,
textColor,
borderColor,
)}
onClick={onClick}
>
<span className="truncate max-w-[100px]">{fileName}</span>
<button className="ml-2 text-muted-foreground hover:text-foreground transition-colors duration-200">
<X size={14} />
</button>
</div>
);
}

View File

@@ -0,0 +1,147 @@
import React, { useState } from "react";
import { TabBar } from "./tab-bar";
import { CodeEditor } from "./code-editor";
import { useAtom } from "jotai";
import { optionsAtom } from "../store";
import { js_beautify } from "js-beautify";
import { singUpString } from "../sign-up";
import { signInString } from "../sign-in";
export default function CodeTabs() {
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.forgetPassword
? `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, "")},`
: ""
}
${
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(),` : ""
}],`
: ""
}
})
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: singUpString,
});
}
const [files, setFiles] = useState(initialFiles);
const [activeFileId, setActiveFileId] = useState(files[0].id);
const handleTabClick = (fileId: string) => {
setActiveFileId(fileId);
};
const handleTabClose = (fileId: string) => {
setFiles(files.filter((file) => file.id !== fileId));
if (activeFileId === fileId) {
setActiveFileId(files[0].id);
}
};
const activeFile = files.find((file) => file.id === activeFileId);
return (
<div className="w-full max-w-3xl mx-auto mt-8 border border-border rounded-md overflow-hidden">
<TabBar
files={files}
activeFileId={activeFileId}
onTabClick={handleTabClick}
onTabClose={handleTabClose}
/>
<div className="bg-background">
{activeFile && (
<CodeEditor
language="typescript"
code={
activeFile.name.endsWith(".ts")
? js_beautify(activeFile.content)
: activeFile.content.replace(/\n{3,}/g, "\n\n")
}
/>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React from "react";
import { CodeTab } from "./code-tabs";
interface File {
id: string;
name: string;
content: string;
}
interface TabBarProps {
files: File[];
activeFileId: string;
onTabClick: (fileId: string) => void;
onTabClose: (fileId: string) => void;
}
export function TabBar({
files,
activeFileId,
onTabClick,
onTabClose,
}: TabBarProps) {
return (
<div className="flex bg-muted border-b border-border">
{files.map((file) => (
<CodeTab
key={file.id}
fileName={file.name}
isActive={file.id === activeFileId}
onClick={() => onTabClick(file.id)}
onClose={() => onTabClose(file.id)}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { PrismTheme } from "prism-react-renderer";
const theme: PrismTheme = {
plain: {
color: "#d0d0d0",
backgroundColor: "#000000", // Changed to true black
},
styles: [
{
types: ["comment", "prolog", "doctype", "cdata"],
style: {
color: "#555555",
fontStyle: "italic",
},
},
{
types: ["namespace"],
style: {
opacity: 0.7,
},
},
{
types: ["string", "attr-value"],
style: {
color: "#8ab4f8", // Darker soft blue for strings
},
},
{
types: ["punctuation", "operator"],
style: {
color: "#888888",
},
},
{
types: [
"entity",
"url",
"symbol",
"number",
"boolean",
"variable",
"constant",
"property",
"regex",
"inserted",
],
style: {
color: "#a0a0a0",
},
},
{
types: ["atrule", "keyword", "attr-name", "selector"],
style: {
color: "#c5c5c5",
fontWeight: "bold",
},
},
{
types: ["function", "deleted", "tag"],
style: {
color: "#7aa2f7", // Darker soft blue for functions
},
},
{
types: ["function-variable"],
style: {
color: "#9e9e9e",
},
},
{
types: ["tag", "selector", "keyword"],
style: {
color: "#cccccc", // Adjusted to a slightly lighter gray for better contrast on true black
},
},
],
};
export default theme;