Skip to content

Commit bcd553c

Browse files
refactor(web): organize home page in separate components
1 parent 86a6eb7 commit bcd553c

File tree

5 files changed

+433
-399
lines changed

5 files changed

+433
-399
lines changed

apps/web/scripts/generate-analytics.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { execSync } from "node:child_process";
2-
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2+
import { mkdtempSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import Papa from "papaparse";
@@ -450,27 +450,23 @@ async function generateAnalyticsData() {
450450
lastUpdated: lastUpdated,
451451
};
452452

453-
// Write minimal file to public folder
454-
const publicDir = join(process.cwd(), "public");
455-
if (!existsSync(publicDir)) {
456-
mkdirSync(publicDir, { recursive: true });
457-
}
458-
const minimalFilePath = join(publicDir, "analytics-minimal.json");
459-
writeFileSync(
460-
minimalFilePath,
461-
JSON.stringify(minimalAnalyticsData, null, 2),
462-
);
463-
464453
console.log("📤 Uploading to Cloudflare R2...");
465454

466455
const tempDir = mkdtempSync(join(tmpdir(), "analytics-"));
467456
const tempFilePath = join(tempDir, "analytics-data.json");
457+
const minimalTempFilePath = join(tempDir, "analytics-minimal.json");
468458

469459
writeFileSync(tempFilePath, JSON.stringify(analyticsData, null, 2));
460+
writeFileSync(
461+
minimalTempFilePath,
462+
JSON.stringify(minimalAnalyticsData, null, 2),
463+
);
470464

471465
const BUCKET_NAME = "bucket";
472466
const key = "analytics-data.json";
467+
const minimalKey = "analytics-minimal.json";
473468
const cmd = `npx wrangler r2 object put "${BUCKET_NAME}/${key}" --file="${tempFilePath}" --remote`;
469+
const minimalCmd = `npx wrangler r2 object put "${BUCKET_NAME}/${minimalKey}" --file="${minimalTempFilePath}" --remote`;
474470

475471
console.log(`Uploading ${tempFilePath} to r2://${BUCKET_NAME}/${key} ...`);
476472
try {
@@ -480,11 +476,21 @@ async function generateAnalyticsData() {
480476
throw err;
481477
}
482478

479+
console.log(
480+
`Uploading ${minimalTempFilePath} to r2://${BUCKET_NAME}/${minimalKey} ...`,
481+
);
482+
try {
483+
execSync(minimalCmd, { stdio: "inherit" });
484+
} catch (err) {
485+
console.error("Failed to upload minimal analytics data:", err);
486+
throw err;
487+
}
488+
483489
console.log(
484490
`✅ Generated optimized analytics data with ${totalRecords} records`,
485491
);
486492
console.log(
487-
"📁 Created minimal analytics file: public/analytics-minimal.json",
493+
"📁 Uploaded minimal analytics file to R2: bucket/analytics-minimal.json",
488494
);
489495
console.log("📤 Uploaded to R2 bucket: bucket/analytics-data.json");
490496
console.log(`🕒 Last data update: ${lastUpdated}`);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Check, ChevronDown, ChevronRight, Copy, Terminal } from "lucide-react";
2+
import Link from "next/link";
3+
import { useState } from "react";
4+
import {
5+
DropdownMenu,
6+
DropdownMenuContent,
7+
DropdownMenuItem,
8+
DropdownMenuTrigger,
9+
} from "@/components/ui/dropdown-menu";
10+
import { cn } from "@/lib/utils";
11+
import PackageIcon from "./icons";
12+
13+
export default function CommandSection() {
14+
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
15+
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("bun");
16+
17+
const commands = {
18+
npm: "npx create-better-t-stack@latest",
19+
pnpm: "pnpm create better-t-stack@latest",
20+
bun: "bun create better-t-stack@latest",
21+
};
22+
23+
const copyCommand = (command: string, packageManager: string) => {
24+
navigator.clipboard.writeText(command);
25+
setCopiedCommand(packageManager);
26+
setTimeout(() => setCopiedCommand(null), 2000);
27+
};
28+
29+
return (
30+
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
31+
<div className="flex h-full flex-col justify-between rounded border border-border p-4">
32+
<div className="mb-4 flex items-center justify-between">
33+
<div className="flex items-center gap-2">
34+
<Terminal className="h-4 w-4 text-primary" />
35+
<span className="font-semibold text-sm">CLI_COMMAND</span>
36+
</div>
37+
<DropdownMenu>
38+
<DropdownMenuTrigger asChild>
39+
<button
40+
type="button"
41+
className="flex items-center gap-2 rounded border border-border px-3 py-1.5 text-xs transition-colors hover:bg-muted/10"
42+
>
43+
<PackageIcon pm={selectedPM} className="h-3 w-3" />
44+
<span>{selectedPM.toUpperCase()}</span>
45+
<ChevronDown className="h-3 w-3" />
46+
</button>
47+
</DropdownMenuTrigger>
48+
<DropdownMenuContent align="end">
49+
{(["bun", "pnpm", "npm"] as const).map((pm) => (
50+
<DropdownMenuItem
51+
key={pm}
52+
onClick={() => setSelectedPM(pm)}
53+
className={cn(
54+
"flex items-center gap-2",
55+
selectedPM === pm && "bg-accent text-background",
56+
)}
57+
>
58+
<PackageIcon pm={pm} className="h-3 w-3" />
59+
<span>{pm.toUpperCase()}</span>
60+
{selectedPM === pm && (
61+
<Check className="ml-auto h-3 w-3 text-background" />
62+
)}
63+
</DropdownMenuItem>
64+
))}
65+
</DropdownMenuContent>
66+
</DropdownMenu>
67+
</div>
68+
69+
<div className="space-y-3">
70+
<div className="flex items-center justify-between rounded border border-border p-3">
71+
<div className="flex items-center gap-2 font-mono text-sm">
72+
<span className="text-primary">$</span>
73+
<span className="text-foreground">{commands[selectedPM]}</span>
74+
</div>
75+
<button
76+
type="button"
77+
onClick={() => copyCommand(commands[selectedPM], selectedPM)}
78+
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs hover:bg-muted/10"
79+
>
80+
{copiedCommand === selectedPM ? (
81+
<Check className="h-3 w-3 text-primary" />
82+
) : (
83+
<Copy className="h-3 w-3" />
84+
)}
85+
{copiedCommand === selectedPM ? "COPIED!" : "COPY"}
86+
</button>
87+
</div>
88+
</div>
89+
</div>
90+
91+
<Link href="/new">
92+
<div className="group flex h-full cursor-pointer flex-col justify-between rounded border border-border p-4 transition-colors hover:bg-muted/10">
93+
<div className="mb-4 flex items-center justify-between">
94+
<div className="flex items-center gap-2">
95+
<ChevronRight className="h-4 w-4 text-primary transition-transform group-hover:translate-x-1" />
96+
<span className="font-semibold text-sm">STACK_BUILDER</span>
97+
</div>
98+
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
99+
INTERACTIVE
100+
</div>
101+
</div>
102+
103+
<div className="space-y-3">
104+
<div className="flex items-center justify-between rounded border border-border p-3">
105+
<div className="flex items-center gap-2 text-sm">
106+
<span className="text-primary"></span>
107+
<span className="text-foreground">
108+
Interactive configuration wizard
109+
</span>
110+
</div>
111+
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
112+
START
113+
</div>
114+
</div>
115+
</div>
116+
</div>
117+
</Link>
118+
</div>
119+
);
120+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import NpmPackage from "./npm-package";
2+
3+
export default function HeroSection() {
4+
return (
5+
<>
6+
<div className="mb-8 flex items-center justify-center">
7+
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-4 md:gap-6">
8+
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
9+
{`
10+
██████╗ ██████╗ ██╗ ██╗
11+
██╔══██╗██╔═══██╗██║ ██║
12+
██████╔╝██║ ██║██║ ██║
13+
██╔══██╗██║ ██║██║ ██║
14+
██║ ██║╚██████╔╝███████╗███████╗
15+
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝`}
16+
</pre>
17+
18+
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
19+
{`
20+
██╗ ██╗ ██████╗ ██╗ ██╗██████╗
21+
╚██╗ ██╔╝██╔═══██╗██║ ██║██╔══██╗
22+
╚████╔╝ ██║ ██║██║ ██║██████╔╝
23+
╚██╔╝ ██║ ██║██║ ██║██╔══██╗
24+
██║ ╚██████╔╝╚██████╔╝██║ ██║
25+
╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝`}
26+
</pre>
27+
28+
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
29+
{`
30+
██████╗ ██╗ ██╗███╗ ██╗
31+
██╔═══██╗██║ ██║████╗ ██║
32+
██║ ██║██║ █╗ ██║██╔██╗ ██║
33+
██║ ██║██║███╗██║██║╚██╗██║
34+
╚██████╔╝╚███╔███╔╝██║ ╚████║
35+
╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═══╝`}
36+
</pre>
37+
38+
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
39+
{`
40+
███████╗████████╗ █████╗ ██████╗██╗ ██╗
41+
██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
42+
███████╗ ██║ ███████║██║ █████╔╝
43+
╚════██║ ██║ ██╔══██║██║ ██╔═██╗
44+
███████║ ██║ ██║ ██║╚██████╗██║ ██╗
45+
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝`}
46+
</pre>
47+
</div>
48+
</div>
49+
50+
<div className="mb-6 text-center">
51+
<p className="mx-auto text-lg text-muted-foreground">
52+
Modern CLI for scaffolding end-to-end type-safe TypeScript projects
53+
</p>
54+
<p className="mx-auto mt-2 max-w-2xl text-muted-foreground text-sm">
55+
Production-ready • Customizable • Best practices included
56+
</p>
57+
<NpmPackage />
58+
</div>
59+
</>
60+
);
61+
}

0 commit comments

Comments
 (0)