refactor: simplify social provider handling and improve layout in Builder and SignIn components

This commit is contained in:
Bereket Engida
2026-01-16 12:23:55 -08:00
committed by Alex Yang
parent 032ead4790
commit c1ae2ed9f3
5 changed files with 116 additions and 224 deletions

View File

@@ -1,10 +1,10 @@
"use client";
import { Check, Copy } from "lucide-react";
import { Highlight } from "prism-react-renderer";
import { useTheme } from "next-themes";
import { Highlight, themes } from "prism-react-renderer";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import theme from "./theme";
interface CodeEditorProps {
code: string;
@@ -13,6 +13,7 @@ interface CodeEditorProps {
export function CodeEditor({ code, language }: CodeEditorProps) {
const [isCopied, setIsCopied] = useState(false);
const { resolvedTheme } = useTheme();
const copyToClipboard = async () => {
try {
@@ -24,9 +25,11 @@ export function CodeEditor({ code, language }: CodeEditorProps) {
}
};
const theme = resolvedTheme === "dark" ? themes.vsDark : themes.vsLight;
return (
<div className="relative">
<div className="dark:bg-bg-white rounded-md overflow-hidden">
<div className="rounded-md overflow-hidden">
<Highlight theme={theme} code={code} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<div className="overflow-auto max-h-[400px]">
@@ -42,7 +45,10 @@ export function CodeEditor({ code, language }: CodeEditorProps) {
className={lineProps.className}
style={lineProps.style}
>
<span className="inline-block w-8 pr-2 text-right mr-4 text-gray-500 select-none sticky left-0 bg-black">
<span
className="inline-block w-8 pr-2 text-right mr-4 select-none sticky left-0 text-muted-foreground"
style={{ backgroundColor: style.backgroundColor }}
>
{i + 1}
</span>
{line.map((token, key) => {

View File

@@ -1,79 +0,0 @@
import type { 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;

View File

@@ -1,7 +1,7 @@
"use client";
import { useAtom } from "jotai";
import { CircleX, ListFilter, Moon, PlusIcon, Sun, X } from "lucide-react";
import { ArrowLeft, Moon, PlusIcon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useMemo, useState } from "react";
import { cn } from "@/lib/utils";
@@ -28,7 +28,6 @@ import {
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { ScrollArea } from "../ui/scroll-area";
import { Switch } from "../ui/switch";
@@ -271,50 +270,6 @@ export function Builder() {
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");
@@ -337,7 +292,7 @@ export function Builder() {
}
return optionValue !== defaultValue;
});
}, [debouncedSearch, options]);
}, [options]);
return (
<Dialog>
@@ -368,7 +323,7 @@ export function Builder() {
<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"
variant="ghost"
className="size-8"
onClick={() => {
if (resolvedTheme === "dark") {
@@ -506,32 +461,9 @@ export function Builder() {
</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>
<AccordionContent className="px-1 pt-1">
<div className="grid gap-2 grid-cols-1 @sm/config:grid-cols-2 @md/config:grid-cols-3 @xl/config:grid-cols-4">
{filteredSocialProviders
{Object.entries(socialProviders)
.sort(([a], [b]) => a.localeCompare(b))
.map(([provider, { Icon }]) => (
<Label
@@ -541,9 +473,9 @@ export function Builder() {
provider,
)}
className={cn(
"px-2.5 py-2 rounded-lg border transition-colors flex flex-col items-center justify-center",
"px-2.5 py-2 border transition-colors border-dashed flex flex-col items-center justify-center",
options.socialProviders.includes(provider)
? "border-primary"
? "border-primary/40"
: "",
)}
>
@@ -577,19 +509,6 @@ export function Builder() {
/>
</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>
@@ -690,36 +609,32 @@ export function Builder() {
</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"
<Button
className="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>
Continue
</Button>
</CardFooter>
</>
)}
{currentStep === 1 && (
<>
<CardHeader>
<CardTitle>Choose Framework</CardTitle>
<p
className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
<CardHeader className="flex flex-row items-center gap-2">
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={() => {
setCurrentStep(0);
}}
>
Go Back
</p>
<ArrowLeft className="size-4" />
</Button>
<CardTitle>Choose Framework</CardTitle>
</CardHeader>
<CardContent className="flex items-start justify-center gap-2 flex-wrap justify-between">
{frameworks.map((fm) => (
@@ -754,16 +669,18 @@ export function Builder() {
{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"
<CardHeader className="flex flex-row items-center gap-2">
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={() => {
setCurrentStep(0);
setCurrentStep(1);
}}
>
Go Back
</p>
<ArrowLeft className="size-4" />
</Button>
<CardTitle>Code</CardTitle>
</CardHeader>
<CardContent>

View File

@@ -100,38 +100,86 @@ export default function SignIn() {
Sign-in with Passkey
</Button>
)}
<div
className={cn(
"w-full gap-2 flex items-center justify-between",
options.socialProviders.length > 3
? "flex-row flex-wrap"
: "flex-col",
)}
>
{Object.keys(socialProviders).map((provider) => {
if (options.socialProviders.includes(provider)) {
<div className="w-full gap-2 flex flex-wrap">
{(() => {
const selectedProviders = options.socialProviders;
const count = selectedProviders.length;
// Layout rules:
// - 1-3 providers: full width, show "Sign in with [Provider]"
// - 4+ providers: wrap in rows with provider names (top) and icons (bottom)
// - namedCount = count % 4 (or 4 if evenly divisible)
// - named providers show just the provider name, 2 per row (1/2 width each)
// - icon-only providers show just icon, 4 per row (1/4 width each)
if (count <= 3) {
// 1-3 providers: full width with "Sign in with [Provider]"
return selectedProviders.map((provider) => {
const { Icon } =
socialProviders[provider as keyof typeof socialProviders];
const providerName =
provider.charAt(0).toUpperCase() + provider.slice(1);
return (
<Button
key={provider}
variant="outline"
className="w-full gap-2"
>
<Icon className="size-4 shrink-0" />
Sign in with {providerName}
</Button>
);
});
}
// 4+ providers: use wrapping layout
const remainder = count % 4;
const namedCount = remainder === 0 ? 4 : remainder;
return selectedProviders.map((provider, index) => {
const { Icon } =
socialProviders[provider as keyof typeof socialProviders];
const isNamed = index < namedCount;
const providerName =
provider.charAt(0).toUpperCase() + provider.slice(1);
// Determine width class and if full width (for text display)
let widthClass: string;
let isFullWidth = false;
if (isNamed) {
// Named providers: 2 per row (1/2 width each)
// If odd number of named providers, first one takes full width
if (namedCount === 1) {
widthClass = "w-full";
isFullWidth = true;
} else if (namedCount % 2 === 1 && index === 0) {
// Odd number of named providers, first one takes full width
widthClass = "w-full";
isFullWidth = true;
} else {
widthClass = "w-[calc(50%-0.25rem)]";
}
} else {
// Icon-only providers: 1/4 width (4 per row)
widthClass = "w-[calc(25%-0.375rem)]";
}
return (
<Button
key={provider}
variant="outline"
className={cn(
options.socialProviders.length > 3
? "flex-grow"
: "w-full gap-2",
)}
className={cn(widthClass, "gap-2")}
>
<Icon width="1.2em" height="1.2em" />
{options.socialProviders.length <= 3 &&
"Sign in with " +
provider.charAt(0).toUpperCase() +
provider.slice(1)}
<Icon className="size-4 shrink-0" />
{isNamed &&
(isFullWidth
? `Sign in with ${providerName}`
: providerName)}
</Button>
);
}
return null;
})}
});
})()}
</div>
</div>
</CardContent>

View File

@@ -4,28 +4,28 @@ export const socialProviders = {
apple: {
Icon: (props?: SVGProps<any>) => (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
viewBox="0 0 512 512"
{...props}
>
<path
fill="currentColor"
d="M17.05 20.28c-.98.95-2.05.8-3.08.35c-1.09-.46-2.09-.48-3.24 0c-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8c1.18-.24 2.31-.93 3.57-.84c1.51.12 2.65.72 3.4 1.8c-3.12 1.87-2.38 5.98.48 7.13c-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25c.29 2.58-2.34 4.5-3.74 4.25"
></path>
d="M462.595 399.003c-7.743 17.888-16.908 34.353-27.527 49.492c-14.474 20.637-26.326 34.923-35.459 42.855c-14.159 13.021-29.329 19.69-45.573 20.068c-11.662 0-25.726-3.318-42.096-10.05c-16.425-6.7-31.519-10.019-45.32-10.019c-14.475 0-29.999 3.318-46.603 10.019c-16.63 6.731-30.027 10.24-40.27 10.587c-15.578.664-31.105-6.195-46.603-20.606c-9.892-8.628-22.265-23.418-37.088-44.372c-15.903-22.375-28.977-48.322-39.221-77.904c-10.969-31.952-16.469-62.892-16.469-92.846c0-34.313 7.414-63.906 22.265-88.706c11.672-19.92 27.199-35.633 46.631-47.169s40.431-17.414 63.043-17.79c12.373 0 28.599 3.827 48.762 11.349c20.107 7.547 33.017 11.375 38.677 11.375c4.232 0 18.574-4.475 42.887-13.397c22.992-8.274 42.397-11.7 58.293-10.35c43.076 3.477 75.438 20.457 96.961 51.05c-38.525 23.343-57.582 56.037-57.203 97.979c.348 32.669 12.199 59.855 35.491 81.44c10.555 10.019 22.344 17.762 35.459 23.26c-2.844 8.248-5.846 16.149-9.038 23.735zM363.801 10.242c0 25.606-9.355 49.514-28.001 71.643c-22.502 26.307-49.719 41.508-79.234 39.11a80 80 0 0 1-.594-9.703c0-24.582 10.701-50.889 29.704-72.398c9.488-10.89 21.554-19.946 36.187-27.17C336.464 4.608 350.275.672 363.264-.001c.379 3.423.538 6.846.538 10.242z"
/>
</svg>
),
stringIcon: `<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M17.05 20.28c-.98.95-2.05.8-3.08.35c-1.09-.46-2.09-.48-3.24 0c-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8c1.18-.24 2.31-.93 3.57-.84c1.51.12 2.65.72 3.4 1.8c-3.12 1.87-2.38 5.98.48 7.13c-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25c.29 2.58-2.34 4.5-3.74 4.25"
></path>
d="M462.595 399.003c-7.743 17.888-16.908 34.353-27.527 49.492c-14.474 20.637-26.326 34.923-35.459 42.855c-14.159 13.021-29.329 19.69-45.573 20.068c-11.662 0-25.726-3.318-42.096-10.05c-16.425-6.7-31.519-10.019-45.32-10.019c-14.475 0-29.999 3.318-46.603 10.019c-16.63 6.731-30.027 10.24-40.27 10.587c-15.578.664-31.105-6.195-46.603-20.606c-9.892-8.628-22.265-23.418-37.088-44.372c-15.903-22.375-28.977-48.322-39.221-77.904c-10.969-31.952-16.469-62.892-16.469-92.846c0-34.313 7.414-63.906 22.265-88.706c11.672-19.92 27.199-35.633 46.631-47.169s40.431-17.414 63.043-17.79c12.373 0 28.599 3.827 48.762 11.349c20.107 7.547 33.017 11.375 38.677 11.375c4.232 0 18.574-4.475 42.887-13.397c22.992-8.274 42.397-11.7 58.293-10.35c43.076 3.477 75.438 20.457 96.961 51.05c-38.525 23.343-57.582 56.037-57.203 97.979c.348 32.669 12.199 59.855 35.491 81.44c10.555 10.019 22.344 17.762 35.459 23.26c-2.844 8.248-5.846 16.149-9.038 23.735zM363.801 10.242c0 25.606-9.355 49.514-28.001 71.643c-22.502 26.307-49.719 41.508-79.234 39.11a80 80 0 0 1-.594-9.703c0-24.582 10.701-50.889 29.704-72.398c9.488-10.89 21.554-19.946 36.187-27.17C336.464 4.608 350.275.672 363.264-.001c.379 3.423.538 6.846.538 10.242z"
/>
</svg>`,
},
dropbox: {