update landing page and fix stack builder logic

This commit is contained in:
Aman Varshney
2025-04-26 21:37:07 +05:30
parent d9fab84aab
commit 089180e400
10 changed files with 1234 additions and 1145 deletions

View File

@@ -1,37 +1,32 @@
"use client";
import { cn } from "@/lib/utils";
import { Check, ClipboardCopy, Terminal } from "lucide-react";
import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { Check, ClipboardCopy } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import PackageIcon from "./Icons";
const CodeContainer = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("bun");
const [copied, setCopied] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const [showCursor, setShowCursor] = useState(true);
const [, setShowCursor] = useState(true);
const [step, setStep] = useState(0);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node))
setIsOpen(false);
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
const interval = setInterval(() => setShowCursor((p) => !p), 500);
return () => clearInterval(interval);
}, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
if (step < 5) {
const timer = setTimeout(
() => setStep((s) => s + 1),
step === 0 ? 1000 : 400,
);
setStep(0);
const initialTimer = setTimeout(() => setStep(1), 1000);
return () => clearTimeout(initialTimer);
}, [selectedPM]);
useEffect(() => {
if (step > 0 && step < 5) {
const timer = setTimeout(() => setStep((s) => s + 1), 400);
return () => clearTimeout(timer);
}
}, [step]);
@@ -42,181 +37,241 @@ const CodeContainer = () => {
bun: "bun create better-t-stack@latest",
};
const copyToClipboard = async (pm: "npm" | "pnpm" | "bun") => {
await navigator.clipboard.writeText(commands[pm]);
setSelectedPM(pm);
const runCommands = {
npm: "npm run dev",
pnpm: "pnpm dev",
bun: "bun dev",
};
const copyToClipboard = async () => {
if (copied) return;
await navigator.clipboard.writeText(commands[selectedPM]);
setCopied(true);
setIsOpen(false);
setTimeout(() => setCopied(false), 2000);
};
const packageManagers: Array<"npm" | "pnpm" | "bun"> = ["bun", "pnpm", "npm"];
return (
<div className="mx-auto mt-4 w-full max-w-3xl">
<div className="overflow-hidden rounded-lg border border-border bg-background">
<div className="flex items-center justify-between bg-muted px-3 py-2">
<div className="flex gap-1.5">
<div className="h-2.5 w-2.5 rounded-full bg-red-500" />
<div className="h-2.5 w-2.5 rounded-full bg-yellow-500" />
<div className="h-2.5 w-2.5 rounded-full bg-green-500" />
<div className="mx-auto mt-6 w-full max-w-3xl font-mono">
<div className="overflow-hidden rounded-lg border border-border bg-muted/30 shadow-sm">
<div className="flex items-center justify-between border-border border-b bg-muted/50 px-4 py-2">
<span className="text-muted-foreground text-xs">
Choose your package manager:
</span>
<div className="flex items-center rounded-md border border-border bg-background p-0.5">
{packageManagers.map((pm) => (
<button
type="button"
key={pm}
onClick={() => setSelectedPM(pm)}
className={cn(
"flex items-center gap-1.5 rounded-[5px] px-2.5 py-1 text-xs transition-colors duration-150",
selectedPM === pm
? "bg-primary/10 text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
<PackageIcon pm={pm} className="size-3.5" />
{pm}
</button>
))}
</div>
</div>
<div className="text-muted-foreground text-xs">Terminal</div>
<div className="relative" ref={menuRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 rounded border border-border bg-background px-2 py-1 text-foreground text-xs hover:bg-muted"
>
<Terminal className="h-3 w-3 text-muted-foreground" />
<span>{selectedPM}</span>
<svg
className="h-3 w-3 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<title>arrow</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d={isOpen ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"}
/>
</svg>
</button>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
className="absolute right-0 z-50 mt-1 w-28 rounded-md border border-border bg-background"
>
{(["npm", "pnpm", "bun"] as const).map((pm) => (
<button
type="button"
key={pm}
className={cn(
"block w-full px-3 py-1.5 text-left text-foreground text-xs",
selectedPM === pm ? "bg-muted" : "hover:bg-muted/50",
)}
onClick={() => copyToClipboard(pm)}
>
{pm === "bun" ? "🥟 bun" : pm}
</button>
))}
</motion.div>
<div className="relative bg-background p-4 text-sm">
<div className="flex items-center gap-2 overflow-x-auto pb-1">
<span className="select-none text-muted-foreground">$</span>
<code className="whitespace-pre text-foreground">
{commands[selectedPM]}
</code>
{step === 0 && (
<motion.span
key="cursor-command"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 1, 0] }}
transition={{
duration: 1,
repeat: Number.POSITIVE_INFINITY,
repeatDelay: 0,
ease: "linear",
}}
className="ml-0.5 inline-block h-4 w-2 flex-shrink-0 bg-foreground"
aria-hidden="true"
/>
)}
</div>
</div>
<div className="bg-background p-4 text-left font-mono text-sm">
<div className="flex items-center">
<span className="mr-2 text-muted-foreground">$</span>
<div className="flex-grow">
<span className="text-foreground">{commands[selectedPM]}</span>
{step === 0 && (
<span
className={cn(
"ml-0.5 inline-block h-4 w-2 bg-foreground",
showCursor ? "opacity-100" : "opacity-0",
)}
/>
)}
</div>
<button
<div className="absolute top-3 right-3">
<motion.button
type="button"
onClick={() => copyToClipboard(selectedPM)}
className="text-muted-foreground hover:text-foreground"
onClick={copyToClipboard}
className={cn(
"flex h-7 w-7 items-center justify-center rounded border bg-background text-muted-foreground transition-all duration-150 hover:border-border hover:bg-muted hover:text-foreground",
copied
? "border-chart-4/50 bg-chart-4/10 text-chart-4"
: "border-border",
)}
aria-label={copied ? "Copied" : "Copy command"}
whileTap={{ scale: 0.9 }}
>
{copied ? (
<Check className="h-4 w-4 text-[--color-chart-4]" />
) : (
<ClipboardCopy className="h-4 w-4" />
)}
</button>
</div>
{step > 0 && (
<div className="mt-3 space-y-1.5 text-sm">
{step > 0 && (
<div className="text-muted-foreground">
Creating a new Better-T-Stack project
</div>
)}
{step > 1 && (
<div className="ml-2 grid grid-cols-[80px_1fr] gap-x-2 text-muted-foreground text-xs">
<span>Project:</span>
<span className="text-foreground">my-app</span>
<span>Frontend:</span>
<span className="text-foreground">React Web</span>
<span>Backend:</span>
<span className="text-foreground">Hono</span>
<span>Database:</span>
<span className="text-foreground">SQLite + Drizzle</span>
</div>
)}
{step > 2 && (
<div className="text-muted-foreground">
Creating project structure
</div>
)}
{step > 3 && (
<div className="text-muted-foreground">
Installing dependencies
</div>
)}
{step > 4 && (
<div className="mt-2 border-border border-l-2 bg-muted py-2 pl-3 text-xs">
<span className="font-semibold text-foreground">
Project created successfully! Run:
</span>
<div className="mt-1 flex flex-wrap gap-1">
<code className="rounded bg-secondary px-1 py-0.5 text-secondary-foreground">
cd my-app
</code>
<span className="text-muted-foreground">and</span>
<code className="rounded bg-secondary px-1 py-0.5 text-secondary-foreground">
{selectedPM === "npm"
? "npm run dev"
: selectedPM === "pnpm"
? "pnpm dev"
: "bun dev"}
</code>
</div>
</div>
)}
</div>
)}
{step > 4 && (
<div className="mt-3 flex items-center">
<span className="mr-2 text-muted-foreground">$</span>
<span
className={cn(
"inline-block h-4 w-2 bg-foreground",
showCursor ? "opacity-100" : "opacity-0",
<AnimatePresence mode="wait" initial={false}>
{copied ? (
<motion.div
key="check"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<Check className="size-4" />
</motion.div>
) : (
<motion.div
key="copy"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<ClipboardCopy className="size-4" />
</motion.div>
)}
/>
</div>
)}
</AnimatePresence>
</motion.button>
</div>
</div>
<div className="border-border border-t bg-muted px-4 py-1.5 text-left text-muted-foreground text-xs">
For customization options:{" "}
<code className="rounded bg-secondary px-1 py-0.5 text-secondary-foreground">
{selectedPM === "npm"
? "npx"
: selectedPM === "pnpm"
? "pnpm dlx"
: "bunx"}{" "}
create-better-t-stack
</code>
</div>
<AnimatePresence>
{step > 0 && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{
height: "auto",
opacity: 1,
transition: {
height: { duration: 0.3 },
opacity: { duration: 0.2, delay: 0.1 },
},
}}
exit={{ height: 0, opacity: 0, transition: { duration: 0.2 } }}
className="overflow-hidden border-border border-t bg-background/70 px-4 pt-3 pb-4"
>
<div className="space-y-1 text-muted-foreground text-xs">
{step >= 1 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
Creating a new Better-T-Stack project...
</motion.div>
)}
{step >= 2 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="pt-1"
>
<div>
<span className="inline-block w-20">Project:</span>
<span className="text-foreground/90">my-app</span>
</div>
<div>
<span className="inline-block w-20">Frontend:</span>
<span className="text-foreground/90">React Web</span>
</div>
<div>
<span className="inline-block w-20">Backend:</span>
<span className="text-foreground/90">Hono</span>
</div>
<div>
<span className="inline-block w-20">Database:</span>
<span className="text-foreground/90">
SQLite + Drizzle
</span>
</div>
</motion.div>
)}
{step >= 3 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
className="flex items-center gap-1.5 pt-1"
>
<Check className="size-3 flex-shrink-0 text-green-500" />
<span>Creating project structure</span>
</motion.div>
)}
{step >= 4 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.3 }}
className="flex items-center gap-1.5"
>
<Check className="size-3 flex-shrink-0 text-green-500" />
<span>Installing dependencies</span>
</motion.div>
)}
{step >= 5 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.4 }}
className="!mt-3 border-green-500 border-l-2 bg-muted/50 py-2 pl-3"
>
<span className="block font-medium text-foreground">
{" "}
Project created successfully! Run:
</span>
<div className="mt-1 flex flex-wrap items-center gap-1.5">
<code className="rounded bg-secondary px-1.5 py-0.5 text-secondary-foreground">
cd my-app
</code>
<span className="text-muted-foreground">&amp;&amp;</span>
<code className="rounded bg-secondary px-1.5 py-0.5 text-secondary-foreground">
{runCommands[selectedPM]}
</code>
</div>
</motion.div>
)}
{step >= 5 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.5 }}
className="!mt-3 flex items-center gap-2"
>
<span className="select-none text-muted-foreground">$</span>
<motion.span
key="cursor-done"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 1, 0] }}
transition={{
duration: 1,
repeat: Number.POSITIVE_INFINITY,
repeatDelay: 0,
ease: "linear",
}}
className="inline-block h-3.5 w-2 flex-shrink-0 bg-foreground"
aria-hidden="true"
/>
</motion.div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="border-border border-t bg-muted/50 px-4 py-2 text-muted-foreground text-xs" />
</div>
</div>
);

View File

@@ -14,7 +14,7 @@ export default function CustomizableSection() {
>
<h2 className="font-bold font-mono text-2xl tracking-tight sm:text-3xl md:text-4xl lg:text-5xl">
<span className="border-primary border-b-2 pb-1 text-foreground dark:text-primary">
Your Stack, Your Choice
Roll Your Own Stack
</span>
</h2>
</motion.div>
@@ -27,8 +27,7 @@ export default function CustomizableSection() {
className="mx-auto max-w-3xl space-y-6"
>
<p className="font-mono text-lg text-muted-foreground leading-relaxed sm:text-xl">
Configure your ideal TypeScript environment with all the options you
need
Build your perfect TypeScript stack.
</p>
</motion.div>
</div>

View File

@@ -1,312 +1,248 @@
"use client";
import { ThemeToggle } from "@/components/theme-toggle";
import { cn } from "@/lib/utils";
import { BookMarked, Github, Maximize2, Menu, X } from "lucide-react";
import { Github, Maximize2, Menu, X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import PackageIcon from "./Icons";
const Navbar = () => {
const [activeLink, setActiveLink] = useState("home");
const [bgStyles, setBgStyles] = useState({});
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
useEffect(() => {
const updateBackground = (linkId: string) => {
const linkElement = linkRefs.current[linkId];
if (linkElement) {
setBgStyles({
padding: "0.75rem 0rem",
width: `${linkElement.clientWidth - 12}px`,
transform: `translateX(${linkElement.offsetLeft}px)`,
opacity: 1,
});
}
};
updateBackground(activeLink);
const handleScroll = () => {
const isScrolled = window.scrollY > 50;
setScrolled(isScrolled);
setScrolled(window.scrollY > 10);
};
window.addEventListener("scroll", handleScroll);
window.addEventListener("resize", () => updateBackground(activeLink));
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", () => updateBackground(activeLink));
document.body.style.overflow = "";
};
}, [activeLink]);
}, [mobileMenuOpen]);
const toggleMobileMenu = () => {
setMobileMenuOpen(!mobileMenuOpen);
};
const closeMobileMenu = () => setMobileMenuOpen(false);
const desktopNavLinks = [
{
href: "/",
label: "Home",
icon: <span className="text-primary">~/</span>,
},
{
href: "https://my-better-t-app-client.pages.dev/",
label: "Demo",
target: "_blank",
},
{ href: "/docs", label: "Docs" },
{
href: "https://www.npmjs.com/package/create-better-t-stack",
label: "NPM",
icon: <PackageIcon pm="npm" className="h-4 w-4" />,
target: "_blank",
},
];
const mobileNavLinks = [
{
href: "/",
label: "Home",
icon: <span className="text-primary">~/</span>,
},
{
href: "https://my-better-t-app-client.pages.dev/",
label: "Demo",
target: "_blank",
},
{ href: "/docs", label: "Docs" },
{
href: "https://www.npmjs.com/package/create-better-t-stack",
label: "NPM",
icon: <PackageIcon pm="npm" className="h-4 w-4" />,
target: "_blank",
},
{
href: "https://www.github.com/better-t-stack/create-better-t-stack",
label: "GitHub",
icon: <Github className="size-4" />,
target: "_blank",
},
];
return (
<>
<nav
className={cn(
"fixed top-0 z-[100] flex w-full items-center justify-between px-4 py-4 transition-all duration-300 sm:px-8",
scrolled ? "bg-transparent" : "bg-background/80 backdrop-blur-xl",
"fixed top-0 z-[100] w-full transition-all duration-300 ease-in-out",
scrolled
? "border-border border-b bg-background/80 shadow-sm backdrop-blur-md"
: "border-transparent border-b bg-transparent",
)}
>
<div
className={cn(
"flex flex-row items-center space-x-3 transition-opacity duration-300",
scrolled ? "opacity-0" : "opacity-100",
)}
>
<div className="flex h-4 w-4 items-center justify-center rounded-sm">
<span className="text-md text-primary">$_</span>
</div>
<span className="font-semibold text-foreground text-md">
Better-T Stack
</span>
</div>
<div className="-translate-x-1/2 absolute left-1/2 hidden transform md:block">
<div
className={cn(
"relative flex items-center rounded-lg border border-border bg-muted/90 px-1.5 py-1 text-sm backdrop-blur-sm transition-all duration-500 ease-out",
scrolled ? "w-[420px]" : "w-[313px]",
)}
>
<div
className="absolute rounded-md bg-background transition-all duration-200 ease-in-out"
style={bgStyles}
/>
<Link
href="/"
ref={(ref) => {
linkRefs.current.home = ref;
}}
onMouseOver={() => setActiveLink("home")}
className="relative flex items-center gap-1 rounded-md px-4 py-2 font-mono text-muted-foreground transition-colors hover:text-primary"
>
<span className="text-primary">~/</span>
home
</Link>
<Link
href="https://my-better-t-app-client.pages.dev/"
target="_blank"
ref={(ref) => {
linkRefs.current.demo = ref;
}}
onMouseOver={() => setActiveLink("demo")}
onMouseLeave={() => setActiveLink("home")}
className="relative flex items-center gap-2 rounded-md px-4 py-2 font-mono text-muted-foreground transition-colors hover:text-primary"
>
<span>demo</span>
</Link>
<Link
href="/docs"
ref={(ref) => {
linkRefs.current.docs = ref;
}}
onMouseOver={() => setActiveLink("docs")}
onMouseLeave={() => setActiveLink("home")}
className="relative flex items-center gap-2 rounded-md px-4 py-2 font-mono text-muted-foreground transition-colors hover:text-primary"
>
<span>docs</span>
</Link>
<Link
href="https://www.npmjs.com/package/create-better-t-stack"
target="_blank"
ref={(ref) => {
linkRefs.current.npm = ref;
}}
onMouseOver={() => setActiveLink("npm")}
onMouseLeave={() => setActiveLink("home")}
className="relative flex items-center gap-2 rounded-md px-4 py-2 font-mono text-muted-foreground transition-colors hover:text-primary"
>
<PackageIcon pm="npm" className="h-4 w-4 rounded-full" />{" "}
<span>npm</span>
</Link>
<Link
href="https://www.github.com/better-t-stack/create-better-t-stack"
target="_blank"
ref={(ref) => {
linkRefs.current.github = ref;
}}
onMouseOver={() => setActiveLink("github")}
onMouseLeave={() => setActiveLink("home")}
className={cn(
"relative flex items-center gap-2 rounded-md px-4 py-2 font-mono text-muted-foreground transition-colors hover:text-primary",
scrolled
? "translate-y-0 opacity-100"
: "pointer-events-none opacity-0",
)}
>
<Github className="size-4">
<title>GitHub</title>
</Github>
Github
</Link>
</div>
</div>
<div
className={cn(
"hidden justify-end gap-2 transition-opacity duration-300 md:flex",
scrolled ? "pointer-events-none opacity-0" : "opacity-100",
)}
>
<Link
href="/new"
className="inline-flex items-center rounded-lg border border-primary/50 bg-primary/10 px-4 py-1 font-mono text-primary text-sm backdrop-blur-sm transition-colors hover:bg-primary/20"
>
<Maximize2 className="mr-1 size-4" />
Stack Builder
</Link>
<Link
href="https://www.github.com/better-t-stack/create-better-t-stack"
target="_blank"
className="inline-flex items-center rounded-lg border border-border bg-muted/90 px-4 py-1 font-mono text-muted-foreground text-sm backdrop-blur-sm transition-colors hover:bg-muted hover:text-primary"
>
<Github className="mr-1 size-4">
<title>GitHub</title>
</Github>
Star on GitHub
</Link>
</div>
<button
type="button"
onClick={toggleMobileMenu}
className="flex items-center justify-center rounded-md p-2 text-foreground hover:bg-muted/50 focus:outline-none md:hidden"
aria-expanded={mobileMenuOpen}
>
{mobileMenuOpen ? (
<X className="size-5" aria-hidden="true" />
) : (
<Menu className="size-5" aria-hidden="true" />
)}
<span className="sr-only">Toggle menu</span>
</button>
</nav>
<div
className={cn(
"fixed inset-0 z-[99] pt-16 backdrop-blur-md transition-all duration-300 ease-in-out md:hidden",
mobileMenuOpen
? "pointer-events-auto opacity-100"
: "pointer-events-none opacity-0",
)}
>
<div className="mx-4 mt-4 overflow-hidden rounded-lg border border-border bg-background/95">
<div className="flex items-center bg-muted px-4 py-2">
<div className="mr-4 flex space-x-2">
<div className="h-3 w-3 rounded-full bg-red-500" />
<div className="h-3 w-3 rounded-full bg-yellow-500" />
<div className="h-3 w-3 rounded-full bg-green-500" />
</div>
<div className="font-mono text-muted-foreground text-sm">
better-t-stack:~
</div>
</div>
<div className="p-4 font-mono text-sm">
<div className="pb-3">
<span className="text-[--color-chart-4]">
user@better-t-stack
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
<Link href="/" className="flex flex-shrink-0 items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-md border border-primary/50 bg-primary/10">
<span className="font-medium font-mono text-primary text-sm">
$_
</span>
<span className="text-muted-foreground">:~$</span>
<span className="ml-2 text-foreground">ls -la</span>
</div>
<span className="hidden font-semibold text-foreground text-md sm:inline-block">
Better-T Stack
</span>
</Link>
<div className="hidden items-center gap-4 md:flex">
<div className="flex items-center gap-1">
{desktopNavLinks.map((link) => (
<Link
key={link.href}
href={link.href}
target={link.target}
className="relative flex items-center gap-1.5 rounded-md px-3 py-1.5 font-mono text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-primary"
>
{link.icon}
<span>{link.label}</span>
</Link>
))}
</div>
<div className="space-y-2 border-border border-l-2 pl-4">
<div className="h-5 w-px bg-border" />
<div className="flex items-center gap-2">
<Link
href="/"
className="block text-primary hover:underline"
onClick={() => setMobileMenuOpen(false)}
href="/new"
className="inline-flex items-center gap-1.5 rounded-md border border-primary/50 bg-primary/10 px-3 py-1.5 font-mono text-primary text-xs transition-colors hover:bg-primary/20"
title="Stack Builder"
>
~/home
<Maximize2 className="size-3.5" />
Builder
</Link>
<Link
href="https://my-better-t-app-client.pages.dev/"
target="_blank"
className="block text-primary hover:underline"
onClick={() => setMobileMenuOpen(false)}
>
~/demo
</Link>
<div className="flex items-center">
<PackageIcon pm="npm" className="mr-1 h-4 w-4" />
<Link
href="https://www.npmjs.com/package/create-better-t-stack"
target="_blank"
className="block text-primary hover:underline"
onClick={() => setMobileMenuOpen(false)}
>
~/npm
</Link>
</div>
<div className="flex items-center">
<BookMarked className="mr-1 h-4 w-4" />
<Link
href="/docs"
className="block text-primary hover:underline"
onClick={() => setMobileMenuOpen(false)}
>
~/docs
</Link>
</div>
<div className="flex items-center">
<Github className="mr-1 size-4 text-foreground" />
<Link
href="https://www.github.com/better-t-stack/create-better-t-stack"
target="_blank"
className="block text-primary hover:underline"
onClick={() => setMobileMenuOpen(false)}
>
~/github
</Link>
</div>
</div>
<div className="mt-6 pb-3">
<span className="text-[--color-chart-4]">
user@better-t-stack
</span>
<span className="text-muted-foreground">:~$</span>
<span className="ml-2 text-foreground">star-repo</span>
</div>
<div className="border-border border-l-2 pb-2 pl-4">
<Link
href="https://www.github.com/better-t-stack/create-better-t-stack"
target="_blank"
className="inline-flex items-center rounded-md bg-muted px-4 py-2 text-foreground transition-colors hover:bg-muted/80"
onClick={() => setMobileMenuOpen(false)}
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-muted/90 px-3 py-1.5 font-mono text-muted-foreground text-xs backdrop-blur-sm transition-colors hover:bg-muted hover:text-foreground"
title="Star on GitHub"
>
<Github className="mr-1 size-5" />
Star on GitHub
<Github className="size-3.5" />
Star
</Link>
</div>
<div className="mt-4">
<span className="text-[--color-chart-4]">
user@better-t-stack
</span>
<span className="text-muted-foreground">:~$</span>
<span className="ml-2 animate-pulse text-foreground"></span>
</div>
<ThemeToggle />
</div>
<div className="flex items-center gap-2 md:hidden">
<ThemeToggle />
<button
type="button"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="flex items-center justify-center rounded-md p-1.5 text-foreground transition-colors hover:bg-muted"
aria-expanded={mobileMenuOpen}
aria-label="Toggle menu"
>
{mobileMenuOpen ? (
<X className="size-5" />
) : (
<Menu className="size-5" />
)}
</button>
</div>
</div>
</div>
</nav>
<AnimatePresence>
{mobileMenuOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className="fixed inset-0 z-[98] bg-background/50 backdrop-blur-sm md:hidden"
onClick={closeMobileMenu}
aria-hidden="true"
/>
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed top-0 right-0 bottom-0 z-[99] h-full w-full max-w-xs overflow-y-auto border-border border-l bg-background shadow-lg md:hidden"
aria-modal="true"
>
<div className="flex h-16 items-center justify-between border-border border-b px-4">
<span className="font-semibold text-foreground text-md">
Navigation
</span>
<button
type="button"
onClick={closeMobileMenu}
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="Close menu"
>
<X className="size-5" />
</button>
</div>
<div className="flex flex-col p-4">
<nav className="flex flex-col space-y-1">
{mobileNavLinks.map((link) => (
<Link
key={link.href}
href={link.href}
target={link.target}
onClick={closeMobileMenu}
className="flex items-center gap-3 rounded-md px-3 py-3 font-mono text-base text-muted-foreground transition-colors hover:bg-muted hover:text-primary"
>
{link.icon ? (
<span className="flex w-5 items-center justify-center">
{link.icon}
</span>
) : (
<span className="w-5" />
)}
<span>{link.label}</span>
</Link>
))}
</nav>
<div className="mt-6 space-y-3 border-border border-t pt-6">
<Link
href="/new"
onClick={closeMobileMenu}
className="flex w-full items-center justify-center gap-2 rounded-md border border-primary/50 bg-primary/10 px-4 py-2.5 font-mono text-primary text-sm transition-colors hover:bg-primary/20"
>
<Maximize2 className="size-4" />
Stack Builder
</Link>
<Link
href="https://www.github.com/better-t-stack/create-better-t-stack"
target="_blank"
onClick={closeMobileMenu}
className="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-muted/90 px-4 py-2.5 font-mono text-muted-foreground text-sm backdrop-blur-sm transition-colors hover:bg-muted hover:text-foreground"
>
<Github className="size-4" />
Star on GitHub
</Link>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
};
export default Navbar;
}

View File

@@ -0,0 +1,59 @@
"use client";
import { motion } from "motion/react";
import Image from "next/image";
export default function SponsorsSection() {
const sectionVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: "easeOut" },
},
};
return (
<motion.section
className="relative z-10 mx-auto w-full max-w-7xl space-y-12 px-4 py-16 sm:px-6 sm:py-24 lg:space-y-16 lg:px-8"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={sectionVariants}
>
<div className="text-center">
<h2 className="font-bold font-mono text-3xl text-foreground tracking-tight sm:text-4xl lg:text-5xl">
<span className="text-primary">Sponsors</span>
</h2>
</div>
<motion.div
className="flex justify-center"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Image
src="https://cdn.jsdelivr.net/gh/amanvarshney01/sponsors@master/sponsorkit/sponsors.svg"
alt="Sponsors"
width={1000}
height={500}
className="h-auto max-w-full"
style={{ colorScheme: "light" }}
/>
</motion.div>
<div className="text-center">
<a
href="https://github.com/sponsors/AmanVarshney01"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-md border border-primary/50 bg-primary/10 px-4 py-2 font-mono text-primary text-sm transition-colors hover:bg-primary/20"
>
Become a Sponsor
</a>
</div>
</motion.section>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { useMemo, useState } from "react";
import { Tweet } from "react-tweet";
const TWEET_IDS = [
@@ -38,175 +38,192 @@ const TWEET_IDS = [
"1906570888897777847",
];
const MAX_VISIBLE_PAGES = 5;
export default function Testimonials() {
const [startIndex, setStartIndex] = useState(0);
const [tweetsPerPage, setTweetsPerPage] = useState(1);
const [tweetsPerPage] = useState(6); // Show 6 tweets per page
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1280) {
setTweetsPerPage(6);
} else if (window.innerWidth >= 768) {
setTweetsPerPage(4);
} else if (window.innerWidth >= 640) {
setTweetsPerPage(2);
} else {
setTweetsPerPage(1);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const getVisibleTweets = () => {
const visible = [];
for (let i = 0; i < tweetsPerPage; i++) {
const index = (startIndex + i) % TWEET_IDS.length;
visible.push(index);
}
return visible;
};
const totalPages = useMemo(
() => Math.ceil(TWEET_IDS.length / tweetsPerPage),
[tweetsPerPage],
);
const currentPage = useMemo(
() => Math.floor(startIndex / tweetsPerPage) + 1,
[startIndex, tweetsPerPage],
);
const handleNext = () => {
setStartIndex((prev) => (prev + tweetsPerPage) % TWEET_IDS.length);
setStartIndex((prev) =>
Math.min(prev + tweetsPerPage, (totalPages - 1) * tweetsPerPage),
);
};
const handlePrev = () => {
setStartIndex((prev) => {
const newIndex = prev - tweetsPerPage;
return newIndex < 0 ? TWEET_IDS.length + newIndex : newIndex;
});
setStartIndex((prev) => Math.max(0, prev - tweetsPerPage));
};
const visibleTweets = getVisibleTweets();
const totalPages = Math.ceil(TWEET_IDS.length / tweetsPerPage);
const currentPage = Math.floor(startIndex / tweetsPerPage) + 1;
const goToPage = (pageNumber: number) => {
setStartIndex((pageNumber - 1) * tweetsPerPage);
};
const visibleTweetIndices = useMemo(() => {
const end = Math.min(startIndex + tweetsPerPage, TWEET_IDS.length);
return Array.from({ length: end - startIndex }, (_, i) => startIndex + i);
}, [startIndex, tweetsPerPage]);
const paginationDots = useMemo(() => {
if (totalPages <= MAX_VISIBLE_PAGES) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const startPage = Math.max(
1,
Math.min(
currentPage - Math.floor(MAX_VISIBLE_PAGES / 2),
totalPages - MAX_VISIBLE_PAGES + 1,
),
);
const endPage = Math.min(totalPages, startPage + MAX_VISIBLE_PAGES - 1);
const pages: (number | string)[] = [];
if (startPage > 1) {
pages.push(1);
if (startPage > 2) pages.push("...");
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) pages.push("...");
pages.push(totalPages);
}
return pages;
}, [totalPages, currentPage]);
const sectionVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: "easeOut" },
},
};
const gridVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
},
};
return (
<section className="relative z-10 mx-auto mt-12 w-full max-w-7xl space-y-8 px-4 sm:mt-20 sm:space-y-16 sm:px-6">
<div className="relative space-y-4 text-center sm:space-y-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
className="relative"
>
<h2 className="font-bold font-mono text-xl tracking-tight sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl">
<span className="border-primary border-b-2 pb-1 text-foreground dark:text-primary">
Developer Feedback
</span>
</h2>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 15 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mx-auto max-w-3xl font-mono text-base text-muted-foreground leading-relaxed sm:text-lg md:text-xl"
>
what devs are saying about Better-T-Stack
</motion.p>
<motion.section
className="relative z-10 mx-auto w-full max-w-7xl space-y-12 px-4 py-16 sm:px-6 sm:py-24 lg:space-y-16 lg:px-8"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={sectionVariants}
>
<div className="text-center">
<h2 className="font-bold font-mono text-3xl text-foreground tracking-tight sm:text-4xl lg:text-5xl">
Loved by <span className="text-primary">Developers</span>
</h2>
<p className="mx-auto mt-4 max-w-2xl font-mono text-lg text-muted-foreground leading-relaxed">
See what people are saying about Better-T-Stack on X.
</p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: 0.3 }}
className="relative mt-4 sm:mt-8"
className={cn("grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3")}
variants={gridVariants}
>
<div className="overflow-hidden rounded-xl border border-border bg-background">
<div className="flex items-center justify-between bg-muted px-2 py-2 sm:px-4">
<div className="flex space-x-1 sm:space-x-2">
<div className="h-2 w-2 rounded-full bg-red-500 sm:h-3 sm:w-3" />
<div className="h-2 w-2 rounded-full bg-yellow-500 sm:h-3 sm:w-3" />
<div className="h-2 w-2 rounded-full bg-green-500 sm:h-3 sm:w-3" />
</div>
<div className="font-mono text-[10px] text-muted-foreground sm:text-xs">
Developer Feedback
</div>
<div className="flex items-center gap-1 sm:gap-2">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handlePrev}
className="flex h-5 w-5 items-center justify-center rounded bg-secondary text-secondary-foreground transition-colors hover:bg-muted sm:h-6 sm:w-6"
title="Previous testimonials"
aria-label="Previous testimonials"
>
<ChevronLeft className="h-3 w-3" />
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleNext}
className="flex h-5 w-5 items-center justify-center rounded bg-secondary text-secondary-foreground transition-colors hover:bg-muted sm:h-6 sm:w-6"
title="Next testimonials"
aria-label="Next testimonials"
>
<ChevronRight className="h-3 w-3" />
</motion.button>
</div>
{visibleTweetIndices.map((index) => (
<div
key={TWEET_IDS[index]}
className="overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-shadow duration-300 hover:shadow-md"
>
<Tweet id={TWEET_IDS[index]} />
</div>
<div className="p-2">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{visibleTweets.map((tweetIndex) => (
<Tweet key={tweetIndex} id={TWEET_IDS[tweetIndex]} />
))}
</div>
</div>
<div className="flex items-center justify-between border-border border-t bg-muted px-2 py-2 sm:p-3">
<div className="flex items-center">
<span className="text-[10px] text-muted-foreground sm:text-xs">
{currentPage}/{totalPages}
</span>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(totalPages, 5) }).map((_, i) => {
const pageNum =
totalPages <= 5
? i
: currentPage <= 3
? i
: currentPage >= totalPages - 1
? totalPages - 5 + i
: currentPage - 3 + i;
const isActive = pageNum === currentPage - 1;
return (
<button
type="button"
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={i}
onClick={() => setStartIndex(pageNum * tweetsPerPage)}
className={cn(
"h-1 w-1 rounded-full transition-colors sm:h-1.5 sm:w-1.5",
isActive
? "bg-primary"
: "bg-muted-foreground/50 hover:bg-muted-foreground/70",
)}
aria-label={`Go to page ${pageNum + 1}`}
/>
);
})}
{totalPages > 5 && (
<span className="text-[8px] text-muted-foreground sm:text-[10px]">
...
</span>
)}
</div>
</div>
</div>
</div>
))}
</motion.div>
</section>
{totalPages > 1 && (
<motion.div
className="mt-10 flex items-center justify-between sm:mt-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.5 }}
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handlePrev}
disabled={currentPage === 1}
className={cn(
"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50",
)}
aria-label="Previous page"
>
<ChevronLeft className="size-4" />
Prev
</motion.button>
<div className="hidden items-center gap-1 sm:flex">
{paginationDots.map((page, index) =>
typeof page === "number" ? (
<button
type="button"
key={`${page}-${
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
index
}`}
onClick={() => goToPage(page)}
className={cn(
"flex h-8 w-8 items-center justify-center rounded-md font-medium text-sm transition-colors",
currentPage === page
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted",
)}
aria-label={`Go to page ${page}`}
aria-current={currentPage === page ? "page" : undefined}
>
{page}
</button>
) : (
<span
key={`ellipsis-${
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
index
}`}
className="flex h-8 w-8 items-center justify-center text-muted-foreground text-sm"
aria-hidden="true"
>
...
</span>
),
)}
</div>
<div className="text-muted-foreground text-sm sm:hidden">
Page {currentPage} of {totalPages}
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleNext}
disabled={currentPage === totalPages}
className={cn(
"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50",
)}
aria-label="Next page"
>
Next
<ChevronRight className="size-4" />
</motion.button>
</motion.div>
)}
</motion.section>
);
}

View File

@@ -8,6 +8,7 @@ import CustomizableSection from "./_components/CustomizableSection";
import Footer from "./_components/Footer";
import Navbar from "./_components/Navbar";
import NpmPackage from "./_components/NpmPackage";
import SponsorsSection from "./_components/SponsorsSection";
import Testimonials from "./_components/Testimonials";
export default function HomePage() {
@@ -118,10 +119,8 @@ export default function HomePage() {
<div className="flex items-center justify-center">
<div className="hidden w-1/3 items-center sm:flex">
<div className="h-px flex-grow bg-gradient-to-r from-transparent via-primary/30 to-primary/50" />
<div className="h-2 w-2 rounded-full bg-primary/60" />
</div>
<div className="px-4 sm:px-6">
<div
className={cn(
@@ -143,14 +142,11 @@ export default function HomePage() {
</svg>
</div>
</div>
<div className="hidden w-1/3 items-center sm:flex">
<div className="h-2 w-2 rounded-full bg-primary/60" />
<div className="h-px flex-grow bg-gradient-to-l from-transparent via-primary/30 to-primary/50" />
</div>
</div>
<div className="mt-6 h-px w-full bg-gradient-to-r from-transparent via-primary/30 to-transparent sm:hidden" />
</div>
</motion.div>
@@ -164,6 +160,16 @@ export default function HomePage() {
>
<Testimonials />
</motion.div>
<motion.div
className="w-full"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.15 }}
variants={sectionVariants}
>
<SponsorsSection />
</motion.div>
</main>
<Footer />
</>

View File

@@ -11,7 +11,7 @@
.react-tweet-theme {
--tweet-container-margin: 0 !important;
@apply !bg-background;
@apply !bg-background !border-none !h-full !border-transparent;
}
.shiny-text {

View File

@@ -8,13 +8,13 @@
"@changesets/cli": "^2.29.2",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"turbo": "^2.5.1",
"turbo": "^2.5.2",
"typescript": "5.7.3",
},
},
"apps/cli": {
"name": "create-better-t-stack",
"version": "2.1.5",
"version": "2.2.1",
"bin": {
"create-better-t-stack": "dist/index.js",
},
@@ -1586,19 +1586,19 @@
"tsup": ["tsup@8.4.0", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.25.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ=="],
"turbo": ["turbo@2.5.1", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.1", "turbo-darwin-arm64": "2.5.1", "turbo-linux-64": "2.5.1", "turbo-linux-arm64": "2.5.1", "turbo-windows-64": "2.5.1", "turbo-windows-arm64": "2.5.1" }, "bin": { "turbo": "bin/turbo" } }, "sha512-LT0wYyT+HY4StvmGMq1k2tHCIwauaWSXwyP+tCUked9vja5xEisW8b8NIJGi9BWH5HYH9Og1DysaQFTf8BiydQ=="],
"turbo": ["turbo@2.5.2", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.2", "turbo-darwin-arm64": "2.5.2", "turbo-linux-64": "2.5.2", "turbo-linux-arm64": "2.5.2", "turbo-windows-64": "2.5.2", "turbo-windows-arm64": "2.5.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Qo5lfuStr6LQh3sPQl7kIi243bGU4aHGDQJUf6ylAdGwks30jJFloc9NYHP7Y373+gGU9OS0faA4Mb5Sy8X9Xw=="],
"turbo-darwin-64": ["turbo-darwin-64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-U9lT1rZ20PQjEYDiNE0aZrU6K+StAE8rood9xn3pV1w+CSby56HkdR2AffzMdFf8iPTeZfcY1qL62rDcCeRPTw=="],
"turbo-darwin-64": ["turbo-darwin-64@2.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2aIl0Sx230nLk+Cg2qSVxvPOBWCZpwKNuAMKoROTvWKif6VMpkWWiR9XEPoz7sHeLmCOed4GYGMjL1bqAiIS/g=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Mp0LeP9JENqHnurGNyD557sndPt2BYUbgzUX87tYIdu/26dHyqlobiRzPpEfkOGB/sV4exhJUJGXB1h72szLQ=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MrFYhK/jYu8N6QlqZtqSHi3e4QVxlzqU3ANHTKn3/tThuwTLbNHEvzBPWSj5W7nZcM58dCqi6gYrfRz6bJZyAA=="],
"turbo-linux-64": ["turbo-linux-64@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Cl2yKumJQAlNG5UA7vjCU6SPBLrcKaGhOjTaUjGHeD9WLL8vh4FwOlhOD2wk7zCUlhpJaM73WHY+oOZGMqmzOg=="],
"turbo-linux-64": ["turbo-linux-64@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-LxNqUE2HmAJQ/8deoLgMUDzKxd5bKxqH0UBogWa+DF+JcXhtze3UTMr6lEr0dEofdsEUYK1zg8FRjglmwlN5YA=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OFpb/9YZJG8v3nttD4K5dxW3bwsZp++oxAykpYsPhp552EX6r+dJrt2dzX3C0azls2JLf/UzTpA83fRoM8mC4g=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-0MI1Ao1q8zhd+UUbIEsrM+yLq1BsrcJQRGZkxIsHFlGp7WQQH1oR3laBgfnUCNdCotCMD6w4moc9pUbXdOR3bg=="],
"turbo-windows-64": ["turbo-windows-64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6XnfSxE8xPETVAlAwfMqCuVuZbq9gXTj8H/Eggv/i3Tjoh2l5xMVTOmg3/zV4RlDtTcwhnvXgXx8LEXrSRZmQQ=="],
"turbo-windows-64": ["turbo-windows-64@2.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-hOLcbgZzE5ttACHHyc1ajmWYq4zKT42IC3G6XqgiXxMbS+4eyVYTL+7UvCZBd3Kca1u4TLQdLQjeO76zyDJc2A=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Nc9abxTCpRL8ejzzIm5j6jze3jFi23ZtU83Fwz2N9StquYHGEi72isyeCkrhzCiUvZZEPlFyFaXOSShcJUK58Q=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-fMU41ABhSLa18H8V3Z7BMCGynQ8x+wj9WyBMvWm1jeyRKgkvUYJsO2vkIsy8m0vrwnIeVXKOIn6eSe1ddlBVqw=="],
"tw-animate-css": ["tw-animate-css@1.2.8", "", {}, "sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g=="],

View File

@@ -19,7 +19,7 @@
"@changesets/cli": "^2.29.2",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"turbo": "^2.5.1",
"turbo": "^2.5.2",
"typescript": "5.7.3"
},
"lint-staged": {