mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
refactor(web): organize home page in separate components
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
@@ -450,27 +450,23 @@ async function generateAnalyticsData() {
|
|||||||
lastUpdated: lastUpdated,
|
lastUpdated: lastUpdated,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write minimal file to public folder
|
|
||||||
const publicDir = join(process.cwd(), "public");
|
|
||||||
if (!existsSync(publicDir)) {
|
|
||||||
mkdirSync(publicDir, { recursive: true });
|
|
||||||
}
|
|
||||||
const minimalFilePath = join(publicDir, "analytics-minimal.json");
|
|
||||||
writeFileSync(
|
|
||||||
minimalFilePath,
|
|
||||||
JSON.stringify(minimalAnalyticsData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("📤 Uploading to Cloudflare R2...");
|
console.log("📤 Uploading to Cloudflare R2...");
|
||||||
|
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "analytics-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "analytics-"));
|
||||||
const tempFilePath = join(tempDir, "analytics-data.json");
|
const tempFilePath = join(tempDir, "analytics-data.json");
|
||||||
|
const minimalTempFilePath = join(tempDir, "analytics-minimal.json");
|
||||||
|
|
||||||
writeFileSync(tempFilePath, JSON.stringify(analyticsData, null, 2));
|
writeFileSync(tempFilePath, JSON.stringify(analyticsData, null, 2));
|
||||||
|
writeFileSync(
|
||||||
|
minimalTempFilePath,
|
||||||
|
JSON.stringify(minimalAnalyticsData, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
const BUCKET_NAME = "bucket";
|
const BUCKET_NAME = "bucket";
|
||||||
const key = "analytics-data.json";
|
const key = "analytics-data.json";
|
||||||
|
const minimalKey = "analytics-minimal.json";
|
||||||
const cmd = `npx wrangler r2 object put "${BUCKET_NAME}/${key}" --file="${tempFilePath}" --remote`;
|
const cmd = `npx wrangler r2 object put "${BUCKET_NAME}/${key}" --file="${tempFilePath}" --remote`;
|
||||||
|
const minimalCmd = `npx wrangler r2 object put "${BUCKET_NAME}/${minimalKey}" --file="${minimalTempFilePath}" --remote`;
|
||||||
|
|
||||||
console.log(`Uploading ${tempFilePath} to r2://${BUCKET_NAME}/${key} ...`);
|
console.log(`Uploading ${tempFilePath} to r2://${BUCKET_NAME}/${key} ...`);
|
||||||
try {
|
try {
|
||||||
@@ -480,11 +476,21 @@ async function generateAnalyticsData() {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Uploading ${minimalTempFilePath} to r2://${BUCKET_NAME}/${minimalKey} ...`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
execSync(minimalCmd, { stdio: "inherit" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to upload minimal analytics data:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Generated optimized analytics data with ${totalRecords} records`,
|
`✅ Generated optimized analytics data with ${totalRecords} records`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
"📁 Created minimal analytics file: public/analytics-minimal.json",
|
"📁 Uploaded minimal analytics file to R2: bucket/analytics-minimal.json",
|
||||||
);
|
);
|
||||||
console.log("📤 Uploaded to R2 bucket: bucket/analytics-data.json");
|
console.log("📤 Uploaded to R2 bucket: bucket/analytics-data.json");
|
||||||
console.log(`🕒 Last data update: ${lastUpdated}`);
|
console.log(`🕒 Last data update: ${lastUpdated}`);
|
||||||
|
|||||||
120
apps/web/src/app/(home)/_components/command-section.tsx
Normal file
120
apps/web/src/app/(home)/_components/command-section.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Check, ChevronDown, ChevronRight, Copy, Terminal } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import PackageIcon from "./icons";
|
||||||
|
|
||||||
|
export default function CommandSection() {
|
||||||
|
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
|
||||||
|
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("bun");
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
npm: "npx create-better-t-stack@latest",
|
||||||
|
pnpm: "pnpm create better-t-stack@latest",
|
||||||
|
bun: "bun create better-t-stack@latest",
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyCommand = (command: string, packageManager: string) => {
|
||||||
|
navigator.clipboard.writeText(command);
|
||||||
|
setCopiedCommand(packageManager);
|
||||||
|
setTimeout(() => setCopiedCommand(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
<div className="flex h-full flex-col justify-between rounded border border-border p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Terminal className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-semibold text-sm">CLI_COMMAND</span>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 rounded border border-border px-3 py-1.5 text-xs transition-colors hover:bg-muted/10"
|
||||||
|
>
|
||||||
|
<PackageIcon pm={selectedPM} className="h-3 w-3" />
|
||||||
|
<span>{selectedPM.toUpperCase()}</span>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{(["bun", "pnpm", "npm"] as const).map((pm) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={pm}
|
||||||
|
onClick={() => setSelectedPM(pm)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2",
|
||||||
|
selectedPM === pm && "bg-accent text-background",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PackageIcon pm={pm} className="h-3 w-3" />
|
||||||
|
<span>{pm.toUpperCase()}</span>
|
||||||
|
{selectedPM === pm && (
|
||||||
|
<Check className="ml-auto h-3 w-3 text-background" />
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between rounded border border-border p-3">
|
||||||
|
<div className="flex items-center gap-2 font-mono text-sm">
|
||||||
|
<span className="text-primary">$</span>
|
||||||
|
<span className="text-foreground">{commands[selectedPM]}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => copyCommand(commands[selectedPM], selectedPM)}
|
||||||
|
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs hover:bg-muted/10"
|
||||||
|
>
|
||||||
|
{copiedCommand === selectedPM ? (
|
||||||
|
<Check className="h-3 w-3 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
{copiedCommand === selectedPM ? "COPIED!" : "COPY"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href="/new">
|
||||||
|
<div className="group flex h-full cursor-pointer flex-col justify-between rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ChevronRight className="h-4 w-4 text-primary transition-transform group-hover:translate-x-1" />
|
||||||
|
<span className="font-semibold text-sm">STACK_BUILDER</span>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
|
||||||
|
INTERACTIVE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between rounded border border-border p-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<span className="text-primary">⚡</span>
|
||||||
|
<span className="text-foreground">
|
||||||
|
Interactive configuration wizard
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
|
||||||
|
START
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
apps/web/src/app/(home)/_components/hero-section.tsx
Normal file
61
apps/web/src/app/(home)/_components/hero-section.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import NpmPackage from "./npm-package";
|
||||||
|
|
||||||
|
export default function HeroSection() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-8 flex items-center justify-center">
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-4 md:gap-6">
|
||||||
|
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
||||||
|
{`
|
||||||
|
██████╗ ██████╗ ██╗ ██╗
|
||||||
|
██╔══██╗██╔═══██╗██║ ██║
|
||||||
|
██████╔╝██║ ██║██║ ██║
|
||||||
|
██╔══██╗██║ ██║██║ ██║
|
||||||
|
██║ ██║╚██████╔╝███████╗███████╗
|
||||||
|
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝`}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
||||||
|
{`
|
||||||
|
██╗ ██╗ ██████╗ ██╗ ██╗██████╗
|
||||||
|
╚██╗ ██╔╝██╔═══██╗██║ ██║██╔══██╗
|
||||||
|
╚████╔╝ ██║ ██║██║ ██║██████╔╝
|
||||||
|
╚██╔╝ ██║ ██║██║ ██║██╔══██╗
|
||||||
|
██║ ╚██████╔╝╚██████╔╝██║ ██║
|
||||||
|
╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝`}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
||||||
|
{`
|
||||||
|
██████╗ ██╗ ██╗███╗ ██╗
|
||||||
|
██╔═══██╗██║ ██║████╗ ██║
|
||||||
|
██║ ██║██║ █╗ ██║██╔██╗ ██║
|
||||||
|
██║ ██║██║███╗██║██║╚██╗██║
|
||||||
|
╚██████╔╝╚███╔███╔╝██║ ╚████║
|
||||||
|
╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═══╝`}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
||||||
|
{`
|
||||||
|
███████╗████████╗ █████╗ ██████╗██╗ ██╗
|
||||||
|
██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
||||||
|
███████╗ ██║ ███████║██║ █████╔╝
|
||||||
|
╚════██║ ██║ ██╔══██║██║ ██╔═██╗
|
||||||
|
███████║ ██║ ██║ ██║╚██████╗██║ ██╗
|
||||||
|
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6 text-center">
|
||||||
|
<p className="mx-auto text-lg text-muted-foreground">
|
||||||
|
Modern CLI for scaffolding end-to-end type-safe TypeScript projects
|
||||||
|
</p>
|
||||||
|
<p className="mx-auto mt-2 max-w-2xl text-muted-foreground text-sm">
|
||||||
|
Production-ready • Customizable • Best practices included
|
||||||
|
</p>
|
||||||
|
<NpmPackage />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
227
apps/web/src/app/(home)/_components/stats-section.tsx
Normal file
227
apps/web/src/app/(home)/_components/stats-section.tsx
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||||
|
import { useNpmDownloadCounter } from "@erquhart/convex-oss-stats/react";
|
||||||
|
import NumberFlow, { continuous } from "@number-flow/react";
|
||||||
|
import { useQuery } from "convex/react";
|
||||||
|
import {
|
||||||
|
BarChart3,
|
||||||
|
Github,
|
||||||
|
Package,
|
||||||
|
Star,
|
||||||
|
Terminal,
|
||||||
|
TrendingUp,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function StatsSection() {
|
||||||
|
const [analyticsData, setAnalyticsData] = useState<{
|
||||||
|
totalProjects: number;
|
||||||
|
avgProjectsPerDay: string;
|
||||||
|
lastUpdated: string | null;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAnalytics = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://r2.amanv.dev/analytics-minimal.json",
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setAnalyticsData(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch analytics data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAnalytics();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const githubRepo = useQuery(api.stats.getGithubRepo, {
|
||||||
|
name: "AmanVarshney01/create-better-t-stack",
|
||||||
|
});
|
||||||
|
const npmPackages = useQuery(api.stats.getNpmPackages, {
|
||||||
|
names: ["create-better-t-stack"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const liveNpmDownloadCount = useNpmDownloadCounter(npmPackages);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8 grid grid-cols-1 gap-4 sm:mb-12 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Link href="/analytics">
|
||||||
|
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<Terminal className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-semibold text-sm sm:text-base">
|
||||||
|
CLI_ANALYTICS.JSON
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
<BarChart3 className="h-3 w-3" />
|
||||||
|
Total Projects
|
||||||
|
</span>
|
||||||
|
<NumberFlow
|
||||||
|
value={analyticsData?.totalProjects || 0}
|
||||||
|
className="font-bold font-mono text-lg text-primary tabular-nums"
|
||||||
|
transformTiming={{
|
||||||
|
duration: 1000,
|
||||||
|
easing: "ease-out",
|
||||||
|
}}
|
||||||
|
trend={1}
|
||||||
|
willChange
|
||||||
|
isolate
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
<TrendingUp className="h-3 w-3" />
|
||||||
|
Avg/Day
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-foreground text-sm">
|
||||||
|
{analyticsData?.avgProjectsPerDay || "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border/50 border-t pt-3">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="font-mono text-muted-foreground">
|
||||||
|
Last Updated
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-accent">
|
||||||
|
{analyticsData?.lastUpdated ||
|
||||||
|
new Date().toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://github.com/AmanVarshney01/create-better-t-stack"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<Github className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-semibold text-sm sm:text-base">
|
||||||
|
GITHUB_REPO.GIT
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
Stars
|
||||||
|
</span>
|
||||||
|
<NumberFlow
|
||||||
|
value={githubRepo?.starCount || 0}
|
||||||
|
className="font-bold font-mono text-lg text-primary tabular-nums"
|
||||||
|
transformTiming={{
|
||||||
|
duration: 800,
|
||||||
|
easing: "ease-out",
|
||||||
|
}}
|
||||||
|
trend={1}
|
||||||
|
willChange
|
||||||
|
isolate
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
Contributors
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-foreground text-sm">
|
||||||
|
{githubRepo?.contributorCount || "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border/50 border-t pt-3">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="font-mono text-muted-foreground">
|
||||||
|
Repository
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-accent">
|
||||||
|
AmanVarshney01/create-better-t-stack
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://www.npmjs.com/package/create-better-t-stack"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<Terminal className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-semibold text-sm sm:text-base">
|
||||||
|
NPM_PACKAGE.JS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
<Package className="h-3 w-3" />
|
||||||
|
Downloads
|
||||||
|
</span>
|
||||||
|
<NumberFlow
|
||||||
|
value={liveNpmDownloadCount?.count || 0}
|
||||||
|
className="font-bold font-mono text-lg text-primary tabular-nums"
|
||||||
|
transformTiming={{
|
||||||
|
duration: liveNpmDownloadCount?.intervalMs || 1000,
|
||||||
|
easing: "linear",
|
||||||
|
}}
|
||||||
|
trend={1}
|
||||||
|
willChange
|
||||||
|
plugins={[continuous]}
|
||||||
|
isolate
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
<TrendingUp className="h-3 w-3" />
|
||||||
|
Avg/Day
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-foreground text-sm">
|
||||||
|
{npmPackages?.dayOfWeekAverages
|
||||||
|
? Math.round(
|
||||||
|
npmPackages.dayOfWeekAverages.reduce((a, b) => a + b, 0) /
|
||||||
|
7,
|
||||||
|
)
|
||||||
|
: "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border/50 border-t pt-3">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="font-mono text-muted-foreground">Package</span>
|
||||||
|
<span className="font-mono text-accent">
|
||||||
|
create-better-t-stack
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,398 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
import CommandSection from "./_components/command-section";
|
||||||
import { useNpmDownloadCounter } from "@erquhart/convex-oss-stats/react";
|
|
||||||
import NumberFlow, { continuous } from "@number-flow/react";
|
|
||||||
import { useQuery } from "convex/react";
|
|
||||||
import {
|
|
||||||
BarChart3,
|
|
||||||
Check,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
Copy,
|
|
||||||
Github,
|
|
||||||
Package,
|
|
||||||
Star,
|
|
||||||
Terminal,
|
|
||||||
TrendingUp,
|
|
||||||
Users,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import analyticsData from "@/public/analytics-minimal.json";
|
|
||||||
import Footer from "./_components/footer";
|
import Footer from "./_components/footer";
|
||||||
import PackageIcon from "./_components/icons";
|
import HeroSection from "./_components/hero-section";
|
||||||
import NpmPackage from "./_components/npm-package";
|
|
||||||
import SponsorsSection from "./_components/sponsors-section";
|
import SponsorsSection from "./_components/sponsors-section";
|
||||||
|
import StatsSection from "./_components/stats-section";
|
||||||
import Testimonials from "./_components/testimonials";
|
import Testimonials from "./_components/testimonials";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
|
|
||||||
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("bun");
|
|
||||||
|
|
||||||
const commands = {
|
|
||||||
npm: "npx create-better-t-stack@latest",
|
|
||||||
pnpm: "pnpm create better-t-stack@latest",
|
|
||||||
bun: "bun create better-t-stack@latest",
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyCommand = (command: string, packageManager: string) => {
|
|
||||||
navigator.clipboard.writeText(command);
|
|
||||||
setCopiedCommand(packageManager);
|
|
||||||
setTimeout(() => setCopiedCommand(null), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const githubRepo = useQuery(api.stats.getGithubRepo, {
|
|
||||||
name: "AmanVarshney01/create-better-t-stack",
|
|
||||||
});
|
|
||||||
const npmPackages = useQuery(api.stats.getNpmPackages, {
|
|
||||||
names: ["create-better-t-stack"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const liveNpmDownloadCount = useNpmDownloadCounter(npmPackages);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-svh max-w-[1280px]">
|
<div className="mx-auto min-h-svh max-w-[1280px]">
|
||||||
<main className="mx-auto px-4 pt-12">
|
<main className="mx-auto px-4 pt-12">
|
||||||
<div className="mb-8 flex items-center justify-center">
|
<HeroSection />
|
||||||
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-4 md:gap-6">
|
<CommandSection />
|
||||||
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
<StatsSection />
|
||||||
{`
|
|
||||||
██████╗ ██████╗ ██╗ ██╗
|
|
||||||
██╔══██╗██╔═══██╗██║ ██║
|
|
||||||
██████╔╝██║ ██║██║ ██║
|
|
||||||
██╔══██╗██║ ██║██║ ██║
|
|
||||||
██║ ██║╚██████╔╝███████╗███████╗
|
|
||||||
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝`}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
|
||||||
{`
|
|
||||||
██╗ ██╗ ██████╗ ██╗ ██╗██████╗
|
|
||||||
╚██╗ ██╔╝██╔═══██╗██║ ██║██╔══██╗
|
|
||||||
╚████╔╝ ██║ ██║██║ ██║██████╔╝
|
|
||||||
╚██╔╝ ██║ ██║██║ ██║██╔══██╗
|
|
||||||
██║ ╚██████╔╝╚██████╔╝██║ ██║
|
|
||||||
╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝`}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
|
||||||
{`
|
|
||||||
██████╗ ██╗ ██╗███╗ ██╗
|
|
||||||
██╔═══██╗██║ ██║████╗ ██║
|
|
||||||
██║ ██║██║ █╗ ██║██╔██╗ ██║
|
|
||||||
██║ ██║██║███╗██║██║╚██╗██║
|
|
||||||
╚██████╔╝╚███╔███╔╝██║ ╚████║
|
|
||||||
╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═══╝`}
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
|
|
||||||
{`
|
|
||||||
███████╗████████╗ █████╗ ██████╗██╗ ██╗
|
|
||||||
██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
|
||||||
███████╗ ██║ ███████║██║ █████╔╝
|
|
||||||
╚════██║ ██║ ██╔══██║██║ ██╔═██╗
|
|
||||||
███████║ ██║ ██║ ██║╚██████╗██║ ██╗
|
|
||||||
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-6 text-center">
|
|
||||||
<p className="mx-auto text-lg text-muted-foreground">
|
|
||||||
Modern CLI for scaffolding end-to-end type-safe TypeScript projects
|
|
||||||
</p>
|
|
||||||
<p className="mx-auto mt-2 max-w-2xl text-muted-foreground text-sm">
|
|
||||||
Production-ready • Customizable • Best practices included
|
|
||||||
</p>
|
|
||||||
<NpmPackage />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
||||||
<div className="flex h-full flex-col justify-between rounded border border-border p-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Terminal className="h-4 w-4 text-primary" />
|
|
||||||
<span className="font-semibold text-sm">CLI_COMMAND</span>
|
|
||||||
</div>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-2 rounded border border-border px-3 py-1.5 text-xs transition-colors hover:bg-muted/10"
|
|
||||||
>
|
|
||||||
<PackageIcon pm={selectedPM} className="h-3 w-3" />
|
|
||||||
<span>{selectedPM.toUpperCase()}</span>
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
{(["bun", "pnpm", "npm"] as const).map((pm) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={pm}
|
|
||||||
onClick={() => setSelectedPM(pm)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2",
|
|
||||||
selectedPM === pm && "bg-accent text-background",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<PackageIcon pm={pm} className="h-3 w-3" />
|
|
||||||
<span>{pm.toUpperCase()}</span>
|
|
||||||
{selectedPM === pm && (
|
|
||||||
<Check className="ml-auto h-3 w-3 text-background" />
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between rounded border border-border p-3">
|
|
||||||
<div className="flex items-center gap-2 font-mono text-sm">
|
|
||||||
<span className="text-primary">$</span>
|
|
||||||
<span className="text-foreground">
|
|
||||||
{commands[selectedPM]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyCommand(commands[selectedPM], selectedPM)}
|
|
||||||
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs hover:bg-muted/10"
|
|
||||||
>
|
|
||||||
{copiedCommand === selectedPM ? (
|
|
||||||
<Check className="h-3 w-3 text-primary" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
{copiedCommand === selectedPM ? "COPIED!" : "COPY"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link href="/new">
|
|
||||||
<div className="group flex h-full cursor-pointer flex-col justify-between rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ChevronRight className="h-4 w-4 text-primary transition-transform group-hover:translate-x-1" />
|
|
||||||
<span className="font-semibold text-sm">STACK_BUILDER</span>
|
|
||||||
</div>
|
|
||||||
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
|
|
||||||
INTERACTIVE
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between rounded border border-border p-3">
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<span className="text-primary">⚡</span>
|
|
||||||
<span className="text-foreground">
|
|
||||||
Interactive configuration wizard
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
|
|
||||||
START
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-8 grid grid-cols-1 gap-4 sm:mb-12 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<Link href="/analytics">
|
|
||||||
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<Terminal className="h-4 w-4 text-primary" />
|
|
||||||
<span className="font-semibold text-sm sm:text-base">
|
|
||||||
CLI_ANALYTICS.JSON
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
<BarChart3 className="h-3 w-3" />
|
|
||||||
Total Projects
|
|
||||||
</span>
|
|
||||||
<NumberFlow
|
|
||||||
value={analyticsData.totalProjects}
|
|
||||||
className="font-bold font-mono text-lg text-primary tabular-nums"
|
|
||||||
transformTiming={{
|
|
||||||
duration: 1000,
|
|
||||||
easing: "ease-out",
|
|
||||||
}}
|
|
||||||
trend={1}
|
|
||||||
willChange
|
|
||||||
isolate
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
<TrendingUp className="h-3 w-3" />
|
|
||||||
Avg/Day
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-foreground text-sm">
|
|
||||||
{analyticsData.avgProjectsPerDay}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-border/50 border-t pt-3">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="font-mono text-muted-foreground">
|
|
||||||
Last Updated
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-accent">
|
|
||||||
{analyticsData.lastUpdated ||
|
|
||||||
new Date().toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="https://github.com/AmanVarshney01/create-better-t-stack"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<Github className="h-4 w-4 text-primary" />
|
|
||||||
<span className="font-semibold text-sm sm:text-base">
|
|
||||||
GITHUB_REPO.GIT
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
<Star className="h-3 w-3" />
|
|
||||||
Stars
|
|
||||||
</span>
|
|
||||||
{githubRepo?.starCount !== undefined ? (
|
|
||||||
<NumberFlow
|
|
||||||
value={githubRepo.starCount}
|
|
||||||
className="font-bold font-mono text-lg text-primary tabular-nums"
|
|
||||||
transformTiming={{
|
|
||||||
duration: 800,
|
|
||||||
easing: "ease-out",
|
|
||||||
}}
|
|
||||||
trend={1}
|
|
||||||
willChange
|
|
||||||
isolate
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-6 w-16 animate-pulse rounded bg-muted/50 font-bold font-mono text-lg text-primary tabular-nums" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
<Users className="h-3 w-3" />
|
|
||||||
Contributors
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-foreground text-sm">
|
|
||||||
{githubRepo?.contributorCount || "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-border/50 border-t pt-3">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="font-mono text-muted-foreground">
|
|
||||||
Repository
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-accent">
|
|
||||||
AmanVarshney01/create-better-t-stack
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="https://www.npmjs.com/package/create-better-t-stack"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<Terminal className="h-4 w-4 text-primary" />
|
|
||||||
<span className="font-semibold text-sm sm:text-base">
|
|
||||||
NPM_PACKAGE.JS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
<Package className="h-3 w-3" />
|
|
||||||
Downloads
|
|
||||||
</span>
|
|
||||||
{liveNpmDownloadCount?.count !== undefined ? (
|
|
||||||
<NumberFlow
|
|
||||||
value={liveNpmDownloadCount.count}
|
|
||||||
className="font-bold font-mono text-lg text-primary tabular-nums"
|
|
||||||
transformTiming={{
|
|
||||||
duration: liveNpmDownloadCount.intervalMs || 1000,
|
|
||||||
easing: "linear",
|
|
||||||
}}
|
|
||||||
trend={1}
|
|
||||||
willChange
|
|
||||||
plugins={[continuous]}
|
|
||||||
isolate
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-6 w-20 animate-pulse rounded bg-muted/50 font-bold font-mono text-lg text-primary tabular-nums" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
<TrendingUp className="h-3 w-3" />
|
|
||||||
Avg/Day
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-foreground text-sm">
|
|
||||||
{npmPackages?.dayOfWeekAverages
|
|
||||||
? Math.round(
|
|
||||||
npmPackages.dayOfWeekAverages.reduce(
|
|
||||||
(a, b) => a + b,
|
|
||||||
0,
|
|
||||||
) / 7,
|
|
||||||
)
|
|
||||||
: "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-border/50 border-t pt-3">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="font-mono text-muted-foreground">
|
|
||||||
Package
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-accent">
|
|
||||||
create-better-t-stack
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SponsorsSection />
|
<SponsorsSection />
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user