mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
update landing page and fix stack builder logic
This commit is contained in:
@@ -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">&&</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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user